Python 70 - Loose Ends
I have some loose end notes in my keyboard tray Jira. Let’s have a look at some of them.
One note says “fleet timers reset”. The concern is this: the game has various timers, how long until the ship or saucer appears, and so on. When the player types “q” to insert a quarter, we reset the fleets:
class Game:
def insert_quarter(self, number_of_ships):
self.fleets.clear()
self.game_over = False
self.score = 0
self.delta_time = 0
class Fleets:
def clear(self):
for fleet in self.fleets:
fleet.clear()
class Fleet:
def clear(self):
self.flyers.clear()
The Fleet class itself does not have timers, but specialized Fleet classes do:
class AsteroidFleet(Fleet):
def __init__(self, asteroids):
super().__init__(asteroids)
self.timer = Timer(u.ASTEROID_DELAY, self.create_wave)
self.asteroids_in_this_wave = 2
class SaucerFleet(Fleet):
def __init__(self, flyers):
super().__init__(flyers)
self.timer = Timer(u.SAUCER_EMERGENCE_TIME, self.bring_in_saucer)
class ShipFleet(Fleet):
ships_remaining = u.SHIPS_PER_QUARTER
game_over = False
def __init__(self, flyers):
super().__init__(flyers)
self.ship_timer = Timer(u.SHIP_EMERGENCE_TIME, self.spawn_ship_when_ready)
ShipFleet.ships_remaining = u.SHIPS_PER_QUARTER
Reading this, I am wondering how we get ships remaining set back to u.SHIPS_PER_QUARTER
. I don’t see where that would be happening.
Ah. We don’t really call insert_quarter
when you type “q”. We do this:
def main_loop(self):
self.game_init()
self.keep_going = False
while self.running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
self.running = False
self.keep_going = False
self.asteroids_tick(self.delta_time)
pygame.display.flip()
self.delta_time = self.clock.tick(60) / 1000
pygame.quit()
return self.keep_going
def asteroids_tick(self, delta_time):
self.fleets.tick(delta_time)
self.control_game(self.ship, delta_time)
self.process_collisions()
self.draw_everything()
if ShipFleet.game_over: self.draw_game_over()
def control_game(self, ship, dt):
keys = pygame.key.get_pressed()
if keys[pygame.K_q]:
self.keep_going = True
self.running = False
So when you type “q”, the call to control_game
sets running
to False, and keep_going
to True. And in main:
if __name__ == "__main__":
keep_going = True
while keep_going:
asteroids_game = Game()
keep_going = asteroids_game.main_loop()
We loop, because main_loop
will return, and because keep_going
is True, we create a whole new game and things continue. I think there is another sticky note here about that. Yes, it says “insert quarter merge up”.
Therefore, the sticky “fleet timers reset” is no longer valid, because we create a whole new Game instance and the timers are in fact reset.
Another note: “reset for new game Fleets -> Fleet …”. That is also no longer applicable, again because we create everything anew. Scrap that.
But let’s do look at merging insert_quarter
back up into where it is called from.
There are two places:
class Game:
def __init__(self, testing=False):
self.init_general_game_values()
self.init_asteroids_game_values()
self.init_fleets()
# self.init_timers()
self.init_pygame_and_display(testing)
if not testing:
self.insert_quarter(u.SHIPS_PER_QUARTER)
# noinspection PyAttributeOutsideInit
def game_init(self):
self.running = True
Saucer.init_for_new_game()
self.insert_quarter(u.SHIPS_PER_QUARTER)
Who calls game_init
?
def main_loop(self):
self.game_init()
self.keep_going = False
while self.running:
...
This looks like a “belt and suspenders and maybe a staple in your tailbone let’s init things a lot to be sure” kind of thing.
This was all put in place before I got the bright idea to create a new Game instance on every play. Let’s first inline insert_quarter
in both places.
class Game:
def __init__(self, testing=False):
self.init_general_game_values()
self.init_asteroids_game_values()
self.init_fleets()
# self.init_timers()
self.init_pygame_and_display(testing)
if not testing:
self.fleets.clear()
self.game_over = False
self.score = 0
self.delta_time = 0
def game_init(self):
self.running = True
Saucer.init_for_new_game()
self.fleets.clear()
self.game_over = False
self.score = 0
self.delta_time = 0
I’m not sure that was necessary, but now let’s remove the single call to game_init
from main_loop
. Tests are all green. Try the game. The game does not start.
That’s interesting, put it back. No, move the two lines from game_init
up into __init__
:
def __init__(self, testing=False):
self.init_general_game_values()
self.init_asteroids_game_values()
self.init_fleets()
# self.init_timers()
self.init_pygame_and_display(testing)
if not testing:
self.running = True
Saucer.init_for_new_game()
self.fleets.clear()
self.game_over = False
self.score = 0
self.delta_time = 0
Tests and game are good. Now remove game_init
as no longer referenced. Then, extract a method from __init__
.
We could have committed here!
class Game:
def __init__(self, testing=False):
self.init_general_game_values()
self.init_asteroids_game_values()
self.init_fleets()
# self.init_timers()
self.init_pygame_and_display(testing)
if not testing:
self.init_for_game_play()
def init_for_game_play(self):
self.running = True
Saucer.init_for_new_game()
self.fleets.clear()
self.game_over = False
self.score = 0
self.delta_time = 0
No let’s not do that. Let’s instead move things up from under the testing flag until we only have whatever makes the game run and the tests stay green.
We have this:
class Game:
def __init__(self, testing=False):
self.init_general_game_values()
self.init_asteroids_game_values()
self.init_fleets()
# self.init_timers()
self.init_pygame_and_display(testing)
if not testing:
self.running = True
Saucer.init_for_new_game()
# self.fleets.clear()
self.game_over = False
self.score = 0
self.delta_time = 0
# noinspection PyAttributeOutsideInit
def init_general_game_values(self):
self.delta_time = 0
self.game_over = False
self.running = False
self.score = 0
I figure the Fleets are clear because they’ve just been created. We’ll want to check the Saucer.init_for_new_game
but it should be redundant. score
, game_over
and delta_time
are dealt with in init_general_game_values
. The difference seems to be running
.
class Game:
def __init__(self, testing=False):
self.init_general_game_values()
self.init_asteroids_game_values()
self.init_fleets()
# self.init_timers()
self.init_pygame_and_display(testing)
if not testing:
self.running = True
else:
self.running = False
# noinspection PyAttributeOutsideInit
def init_general_game_values(self):
self.delta_time = 0
self.score = 0
We could have committed here!
This works, and of course we see how to improve it. Inline the general stuff and change the if, and we see this:
class Game:
def __init__(self, testing=False):
self.delta_time = 0
self.score = 0
self.init_asteroids_game_values()
self.init_fleets()
self.init_pygame_and_display(testing)
self.running = not testing
# noinspection PyAttributeOutsideInit
def init_asteroids_game_values(self):
pass
That init looks removable. We now have:
class Game:
def __init__(self, testing=False):
self.delta_time = 0
self.score = 0
self.init_fleets()
self.init_pygame_and_display(testing)
self.running = not testing
# noinspection PyAttributeOutsideInit
def init_fleets(self):
asteroids = []
missiles = []
saucers = []
saucer_missiles = []
self.ship = Ship(pygame.Vector2(u.SCREEN_SIZE / 2, u.SCREEN_SIZE / 2))
ships = []
self.fleets = Fleets(asteroids, missiles, saucers, saucer_missiles, ships)
I think we need a commit: simplifying game initialization.
Now I think that Game’s ship is no longer used.
The only usage of self.ship
is here:
def asteroids_tick(self, delta_time):
self.fleets.tick(delta_time)
self.control_game(self.ship, delta_time)
self.process_collisions()
self.draw_everything()
if ShipFleet.game_over: self.draw_game_over()
def control_game(self, ship, dt):
keys = pygame.key.get_pressed()
if keys[pygame.K_q]:
self.keep_going = True
self.running = False
control_game
no longer uses either parm, so we can change its signature.
def control_game(self):
keys = pygame.key.get_pressed()
if keys[pygame.K_q]:
self.keep_going = True
self.running = False
Now self.ship
has no uses. Remove that line.
def init_fleets(self):
asteroids = []
missiles = []
saucers = []
saucer_missiles = []
ships = []
# noinspection PyAttributeOutsideInit
self.fleets = Fleets(asteroids, missiles, saucers, saucer_missiles, ships)
Time to test and commit: simplifying game initialization. (Again)
I am inclined to change Fleets so that you only pass it collections if you’re trying to set it up with existing objects, since the normal starting point is now with all its collections empty.
Fleets starts like this now:
class Fleets:
def __init__(self, asteroids, missiles, saucers, saucer_missiles, ships):
self.fleets = (AsteroidFleet(asteroids), MissileFleet(missiles, u.MISSILE_LIMIT), SaucerFleet(saucers), MissileFleet(saucer_missiles, u.SAUCER_MISSILE_LIMIT), ShipFleet(ships))
I’d like to have the Game just say Fleets()
and leave the details up to Fleets.
To do that, I can’t do this:
class Fleets:
def __init__(self,
asteroids=[],
missiles=[],
saucers=[],
saucer_missiles=[],
ships=[]):
From my understanding of Python, the defaults are created at compile time, so those collections would persist even across new instances of Game and Fleets, so that once they get contents, the contents would stick around. The right way is like this:
class Fleets:
def __init__(self,
asteroids=None,
missiles=None,
saucers=None, s
aucer_missiles=None,
ships=None):
asteroids = asteroids if asteroids is not None else []
missiles = missiles if missiles is not None else []
saucers = saucers if saucers is not None else []
saucer_missiles = saucer_missiles if saucer_missiles is not None else []
ships = ships if ships is not None else []
self.fleets = (AsteroidFleet(asteroids), MissileFleet(missiles, u.MISSILE_LIMIT), SaucerFleet(saucers), MissileFleet(saucer_missiles, u.SAUCER_MISSILE_LIMIT), ShipFleet(ships))
We have to check is not None
because test users sometimes send in an empty collection []
and then check to see if it gets filled, so we cannot just say, for example:
asteroids = asteroids if asteroids else []
We could have committed here!
We are green. Now we can change this:
def init_fleets(self):
asteroids = []
missiles = []
saucers = []
saucer_missiles = []
ships = []
# noinspection PyAttributeOutsideInit
self.fleets = Fleets(asteroids, missiles, saucers, saucer_missiles, ships)
We could have committed here!
First to this:
def init_fleets(self):
# noinspection PyAttributeOutsideInit
self.fleets = Fleets()
We could have committed here!
Then let’s inline its only use:
class Game:
def __init__(self, testing=False):
self.delta_time = 0
self.score = 0
self.fleets = Fleets()
self.init_pygame_and_display(testing)
self.running = not testing
We’re green and good. Commit: game init massively simplified.
Let’s sum up.
Summary
Two elapsed hours while writing, holding a conversation, giving the cat a treat. Just a series of simple steps simplifying things.
I think I could have started committing sooner and perhaps done a few more. The last three were 20 minutes apart, which is pretty good I think, but the first commit was 300 lines into the 425 I have right now, and I’m sure there were opportunities sooner. I’ve marked at least some of the places where we could have committed if I were on the ball.
In my defense, your honor, I was just kind of exploring and trying things for a while there, before we really got going.
In any case, the init logic has been trimmed down substantially, although if PyCharm can give me a line count history I do not know how to get it. It would be interesting to know how many lines, methods, and so on we removed.
In any case, we’ve done well, greatly simplifying the init logic and reducing it down pretty close to minimum.
A nice afternoon of trimming. See you next time!