Python Asteroids+Invaders on GitHub

We’ll review the saucer code and improve it.

Let’s review the saucer code a bit

Recall that we just call the two independent init methods without looking much at what they do. That worked quite nicely but now we want to make Saucer more clear. I think we’ll just inline all the initialization, starting here:

class InvadersSaucer(InvadersFlyer):
    def __init__(self, shot_count=0):
        self._initialize_what_we_can()
        self._finish_initializing(shot_count)

    def _initialize_what_we_can(self):
        maker = BitmapMaker.instance()
        self._map = maker.saucer
        self._mask = pygame.mask.from_surface(self._map)
        self._rect = self._map.get_rect()
        half_width = self._rect.width // 2
        self._left = u.BUMPER_LEFT + half_width
        self._right = u.BUMPER_RIGHT - half_width
        self._speed = 0
        self._player_shot_count = 0
        self._score_list = [100, 50, 50, 100, 150, 100, 100, 50, 300, 100, 100, 100, 50, 150, 100]

    def _finish_initializing(self, shot_count):
        self._set_start_and_speed(shot_count)

We inline to this:

class InvadersSaucer(InvadersFlyer):
    def __init__(self, shot_count=0):
        maker = BitmapMaker.instance()
        self._map = maker.saucer
        self._mask = pygame.mask.from_surface(self._map)
        self._rect = self._map.get_rect()
        half_width = self._rect.width // 2
        self._left = u.BUMPER_LEFT + half_width
        self._right = u.BUMPER_RIGHT - half_width
        self._speed = 0
        self._player_shot_count = 0
        self._score_list = [100, 50, 50, 100, 150, 100, 100, 50, 300, 100, 100, 100, 50, 150, 100]
        self._set_start_and_speed(shot_count)

    def _set_start_and_speed(self, shot_count):
        speed = 8
        even_or_odd = shot_count % 2
        self._speed = (-speed, speed)[even_or_odd]
        left_or_right = (self._right, self._left)[even_or_odd]
        self.rect.center = Vector2(left_or_right, u.INVADER_SAUCER_Y)

Let’s inline once more:

class InvadersSaucer(InvadersFlyer):
    def __init__(self, shot_count=0):
        maker = BitmapMaker.instance()
        self._map = maker.saucer
        self._mask = pygame.mask.from_surface(self._map)
        self._rect = self._map.get_rect()
        half_width = self._rect.width // 2
        self._left = u.BUMPER_LEFT + half_width
        self._right = u.BUMPER_RIGHT - half_width
        self._speed = 0
        self._player_shot_count = 0
        self._score_list = [100, 50, 50, 100, 150, 100, 100, 50, 300, 100, 100, 100, 50, 150, 100]
        speed = 8
        even_or_odd = shot_count % 2
        self._speed = (-speed, speed)[even_or_odd]
        left_or_right = (self._right, self._left)[even_or_odd]
        self.rect.center = Vector2(left_or_right, u.INVADER_SAUCER_Y)

Now let’s extract a few things.

class InvadersSaucer(InvadersFlyer):
    def __init__(self, shot_count=0):
        self.init_map_mask_rect()
        half_width = self._rect.width // 2
        self._left = u.BUMPER_LEFT + half_width
        self._right = u.BUMPER_RIGHT - half_width
        self._speed = 0
        self._player_shot_count = 0
        self._score_list = [100, 50, 50, 100, 150, 100, 100, 50, 300, 100, 100, 100, 50, 150, 100]
        speed = 8
        even_or_odd = shot_count % 2
        self._speed = (-speed, speed)[even_or_odd]
        left_or_right = (self._right, self._left)[even_or_odd]
        self.rect.center = Vector2(left_or_right, u.INVADER_SAUCER_Y)

    # noinspection PyAttributeOutsideInit
    def init_map_mask_rect(self):
        maker = BitmapMaker.instance()
        self._map = maker.saucer
        self._mask = pygame.mask.from_surface(self._map)
        self._rect = self._map.get_rect()

Could be committing right along here, but I’ll hold off a bit. Let’s rename the left and right members.

class InvadersSaucer(InvadersFlyer):
    def __init__(self, shot_count=0):
        self.init_map_mask_rect()
        half_width = self._rect.width // 2
        self._x_minimum = u.BUMPER_LEFT + half_width
        self._x_maximum = u.BUMPER_RIGHT - half_width
        self._speed = 0
        self._player_shot_count = 0
        self._score_list = [100, 50, 50, 100, 150, 100, 100, 50, 300, 100, 100, 100, 50, 150, 100]
        speed = 8
        even_or_odd = shot_count % 2
        self._speed = (-speed, speed)[even_or_odd]
        left_or_right = (self._x_maximum, self._x_minimum)[even_or_odd]
        self.rect.center = Vector2(left_or_right, u.INVADER_SAUCER_Y)

Now let’s compute the score right here. Wait! Something I don’t know: is the mystery score a function of the running shot count or the initial one. It’s probably the running one. We should init _player_shot_count to shot_count, but keep the calculation dynamic after that. I think there is a hidden defect here, which is that as things stand you’re not likely ever to get that 300 score because we start the count at zero each time. This will fix that. No one here has ever hit the saucer more than twice, so didn’t notice the problem.

We may need a test for that.

class InvadersSaucer(InvadersFlyer):
    def __init__(self, shot_count=0):
        self.init_map_mask_rect()
        half_width = self._rect.width // 2
        self._x_minimum = u.BUMPER_LEFT + half_width
        self._x_maximum = u.BUMPER_RIGHT - half_width
        self._speed = 0
        self._player_shot_count = shot_count
        self._score_list = [100, 50, 50, 100, 150, 100, 100, 50, 300, 100, 100, 100, 50, 150, 100]
        speed = 8
        even_or_odd = shot_count % 2
        self._speed = (-speed, speed)[even_or_odd]
        left_or_right = (self._x_maximum, self._x_minimum)[even_or_odd]
        self.rect.center = Vector2(left_or_right, u.INVADER_SAUCER_Y)

The _speed is odd because we set it to zero and then later to the real value. The zero was just there to satisfy the compiler.

Rename left_or_right.

class InvadersSaucer(InvadersFlyer):
    def __init__(self, shot_count=0):
        self.init_map_mask_rect()
        half_width = self._rect.width // 2
        self._x_minimum = u.BUMPER_LEFT + half_width
        self._x_maximum = u.BUMPER_RIGHT - half_width
        self._player_shot_count = shot_count
        self._score_list = [100, 50, 50, 100, 150, 100, 100, 50, 300, 100, 100, 100, 50, 150, 100]
        speed = 8
        even_or_odd = shot_count % 2
        self._speed = (-speed, speed)[even_or_odd]
        starting_x_coordinate = (self._x_maximum, self._x_minimum)[even_or_odd]
        self.rect.center = Vector2(starting_x_coordinate, u.INVADER_SAUCER_Y)

Make the speed a u constant:

class InvadersSaucer(InvadersFlyer):
    def __init__(self, shot_count=0):
        self.init_map_mask_rect()
        half_width = self._rect.width // 2
        self._x_minimum = u.BUMPER_LEFT + half_width
        self._x_maximum = u.BUMPER_RIGHT - half_width
        self._player_shot_count = shot_count
        self._score_list = [100, 50, 50, 100, 150, 100, 100, 50, 300, 100, 100, 100, 50, 150, 100]
        even_or_odd = shot_count % 2
        self._speed = (-u.INVADER_SPEED, u.INVADER_SPEED)[even_or_odd]
        starting_x_coordinate = (self._x_maximum, self._x_minimum)[even_or_odd]
        self.rect.center = Vector2(starting_x_coordinate, u.INVADER_SAUCER_Y)

Extract method:

class InvadersSaucer(InvadersFlyer):
    def __init__(self, shot_count=0):
        self.init_map_mask_rect()
        half_width = self._rect.width // 2
        self._x_minimum = u.BUMPER_LEFT + half_width
        self._x_maximum = u.BUMPER_RIGHT - half_width
        self._player_shot_count = shot_count
        self._score_list = [100, 50, 50, 100, 150, 100, 100, 50, 300, 100, 100, 100, 50, 150, 100]
        self.set_start_and_direction(shot_count)

    # noinspection PyAttributeOutsideInit
    def set_start_and_direction(self, shot_count):
        even_or_odd = shot_count % 2
        starting_x_coordinate = (self._x_maximum, self._x_minimum)[even_or_odd]
        self.rect.center = Vector2(starting_x_coordinate, u.INVADER_SAUCER_Y)
        self._speed = (-u.INVADER_SPEED, u.INVADER_SPEED)[even_or_odd]

And extract:

class InvadersSaucer(InvadersFlyer):
    def __init__(self, shot_count=0):
        self.init_map_mask_rect()
        self.set_motion_limits()
        self._player_shot_count = shot_count
        self._score_list = [100, 50, 50, 100, 150, 100, 100, 50, 300, 100, 100, 100, 50, 150, 100]
        self.set_start_and_direction(shot_count)

    # noinspection PyAttributeOutsideInit
    def set_motion_limits(self):
        half_width = self._rect.width // 2
        self._x_minimum = u.BUMPER_LEFT + half_width
        self._x_maximum = u.BUMPER_RIGHT - half_width

And I think I’ll move the two naked ones up to the top. I don’t see a good name for extracting them. No, wait, how about this:

class InvadersSaucer(InvadersFlyer):
    def __init__(self, shot_count=0):
        self.init_map_mask_rect()
        self.init_for_scoring(shot_count)
        self.set_start_and_direction(shot_count)
        self.set_motion_limits()

    def init_for_scoring(self, shot_count):
        self._player_shot_count = shot_count
        self._score_list = [100, 50, 50, 100, 150, 100, 100, 50, 300, 100, 100, 100, 50, 150, 100]

Then rename the set ones to init for consistency:

class InvadersSaucer(InvadersFlyer):
    def __init__(self, shot_count=0):
        self.init_map_mask_rect()
        self.init_for_scoring(shot_count)
        self.init_start_and_direction(shot_count)
        self.init_motion_limits()

I am liking this. Some tests have broken. I confess that I’m not sure when. I’ll take a quick look, otherwise it’s a do-over. Ha. Problem was that I changed the order of the init calls. This runs:

class InvadersSaucer(InvadersFlyer):
    def __init__(self, shot_count=0):
        self.init_map_mask_rect()
        self.init_for_scoring(shot_count)
        self.init_motion_limits()
        self.init_start_and_direction(shot_count)

Temporal Coupling!

This is a code sign, by the way. The methods are temporally coupled: the order matters. We may wish to deal with that, but first let’s test in game and see if we can commit. We are good. Commit: refactoring for clarity.

We have this:

class InvadersSaucer(InvadersFlyer):
    def __init__(self, shot_count=0):
        self.init_map_mask_rect()
        self.init_for_scoring(shot_count)
        self.init_motion_limits()
        self.init_start_and_direction(shot_count)

These are definitely temporally coupled. Let’s look at all the called methods to observe that. No, let’s not. Let’s inline everything and do it over, better.

Note
It seemed like a good idea to just re-inline it all and do it another way. Possibly there might have been a shorter path, but this way was pretty easy and didn’t require extensive reasoning.
class InvadersSaucer(InvadersFlyer):
    def __init__(self, shot_count=0):
        maker = BitmapMaker.instance()
        self._map = maker.saucer
        self._mask = pygame.mask.from_surface(self._map)
        self._rect = self._map.get_rect()
        self._player_shot_count = shot_count
        self._score_list = [100, 50, 50, 100, 150, 100, 100, 50, 300, 100, 100, 100, 50, 150, 100]
        half_width = self._rect.width // 2
        self._x_minimum = u.BUMPER_LEFT + half_width
        self._x_maximum = u.BUMPER_RIGHT - half_width
        even_or_odd = shot_count % 2
        starting_x_coordinate = (self._x_maximum, self._x_minimum)[even_or_odd]
        self.rect.center = Vector2(starting_x_coordinate, u.INVADER_SAUCER_Y)
        self._speed = (-u.INVADER_SPEED, u.INVADER_SPEED)[even_or_odd]

I note that we have getter and setter for position:

    @property
    def position(self):
        return Vector2(self.rect.center)

    @position.setter
    def position(self, value):
        self.rect.center = value

We really ought to use that setter:

class InvadersSaucer(InvadersFlyer):
    def __init__(self, shot_count=0):
        maker = BitmapMaker.instance()
        self._map = maker.saucer
        self._mask = pygame.mask.from_surface(self._map)
        self._rect = self._map.get_rect()
        self._player_shot_count = shot_count
        self._score_list = [100, 50, 50, 100, 150, 100, 100, 50, 300, 100, 100, 100, 50, 150, 100]
        half_width = self._rect.width // 2
        self._x_minimum = u.BUMPER_LEFT + half_width
        self._x_maximum = u.BUMPER_RIGHT - half_width
        even_or_odd = shot_count % 2
        starting_x_coordinate = (self._x_maximum, self._x_minimum)[even_or_odd]
        self.position = Vector2(starting_x_coordinate, u.INVADER_SAUCER_Y)
        self._speed = (-u.INVADER_SPEED, u.INVADER_SPEED)[even_or_odd]

Why not fully initialize our rect in one go? Let’s move the position-setting up with the rest:

class InvadersSaucer(InvadersFlyer):
    def __init__(self, shot_count=0):
        maker = BitmapMaker.instance()
        self._map = maker.saucer
        self._mask = pygame.mask.from_surface(self._map)
        self._rect = self._map.get_rect()
        self.position = Vector2(starting_x_coordinate, u.INVADER_SAUCER_Y)
        self._player_shot_count = shot_count
        self._score_list = [100, 50, 50, 100, 150, 100, 100, 50, 300, 100, 100, 100, 50, 150, 100]
        half_width = self._rect.width // 2
        self._x_minimum = u.BUMPER_LEFT + half_width
        self._x_maximum = u.BUMPER_RIGHT - half_width
        even_or_odd = shot_count % 2
        starting_x_coordinate = (self._x_maximum, self._x_minimum)[even_or_odd]
        self._speed = (-u.INVADER_SPEED, u.INVADER_SPEED)[even_or_odd]

When we do that, we find that we need to move up all the stuff about half_width and starting x. So we do.

class InvadersSaucer(InvadersFlyer):
    def __init__(self, shot_count=0):
        maker = BitmapMaker.instance()
        self._map = maker.saucer
        self._mask = pygame.mask.from_surface(self._map)
        self._rect = self._map.get_rect()
        half_width = self._rect.width // 2
        self._x_minimum = u.BUMPER_LEFT + half_width
        self._x_maximum = u.BUMPER_RIGHT - half_width
        even_or_odd = shot_count % 2
        starting_x_coordinate = (self._x_maximum, self._x_minimum)[even_or_odd]
        self.position = Vector2(starting_x_coordinate, u.INVADER_SAUCER_Y)
        self._player_shot_count = shot_count
        self._score_list = [100, 50, 50, 100, 150, 100, 100, 50, 300, 100, 100, 100, 50, 150, 100]
        self._speed = (-u.INVADER_SPEED, u.INVADER_SPEED)[even_or_odd]

I can’t move that block higher because of half_width. The whole x min x max stuff is constant, shouldn’t really be here at all. Make some constants:

u.py
BOTTOM_LINE_OFFSET = 50
BUMPER_LEFT = 64
BUMPER_RIGHT = 960
INVADER_PLAYER_Y = SCREEN_SIZE - 128
INVADER_SAUCER_Y = 128
INVADER_SAUCER_HALF_WIDTH = 48
INVADER_SAUCER_X_MIN = BUMPER_LEFT + INVADER_SAUCER_HALF_WIDTH
INVADER_SAUCER_X_MAX = BUMPER_RIGHT - INVADER_SAUCER_HALF_WIDTH
INVADER_SPEED = 8
RESERVE_PLAYER_Y = SCREEN_SIZE - 32
SHIELD_OFFSET = 208
SHIELD_Y = SCREEN_SIZE - 208


class InvadersSaucer(InvadersFlyer):
    def __init__(self, shot_count=0):
        maker = BitmapMaker.instance()
        self._map = maker.saucer
        self._mask = pygame.mask.from_surface(self._map)
        self._rect = self._map.get_rect()
        even_or_odd = shot_count % 2
        starting_x_coordinate = (u.INVADER_SAUCER_X_MAX, u.INVADER_SAUCER_X_MIN)[even_or_odd]
        self._speed = (-u.INVADER_SPEED, u.INVADER_SPEED)[even_or_odd]
        self.position = Vector2(starting_x_coordinate, u.INVADER_SAUCER_Y)
        self._player_shot_count = shot_count
        self._score_list = [100, 50, 50, 100, 150, 100, 100, 50, 300, 100, 100, 100, 50, 150, 100]

Let’s move the scoring stuff to the top and extract it. No, wait, the score list should be in u.

class InvadersSaucer(InvadersFlyer):
    def __init__(self, shot_count=0):
        self._player_shot_count = shot_count
        maker = BitmapMaker.instance()
        self._map = maker.saucer
        self._mask = pygame.mask.from_surface(self._map)
        self._rect = self._map.get_rect()
        even_or_odd = shot_count % 2
        starting_x_coordinate = (u.INVADER_SAUCER_X_MAX, u.INVADER_SAUCER_X_MIN)[even_or_odd]
        self._speed = (-u.INVADER_SPEED, u.INVADER_SPEED)[even_or_odd]
        self.position = Vector2(starting_x_coordinate, u.INVADER_SAUCER_Y)

    def _mystery_score(self):
        score_index = self._player_shot_count % len(u.INVADER_SAUCER_SCORE_LIST)
        return u.INVADER_SAUCER_SCORE_LIST[score_index]

Now we still have that dependency on _rect and position. Let’s make setters for mask and rect, see if that makes things more clear.

class InvadersSaucer(InvadersFlyer):
    def __init__(self, shot_count=0):
        self._player_shot_count = shot_count
        maker = BitmapMaker.instance()
        self._map = maker.saucer
        self.mask = pygame.mask.from_surface(self._map)
        self.rect = self._map.get_rect()
        even_or_odd = shot_count % 2
        starting_x_coordinate = (u.INVADER_SAUCER_X_MAX, u.INVADER_SAUCER_X_MIN)[even_or_odd]
        self._speed = (-u.INVADER_SPEED, u.INVADER_SPEED)[even_or_odd]
        self.position = Vector2(starting_x_coordinate, u.INVADER_SAUCER_Y)

Ah. Right. Extract variable from the last line.

class InvadersSaucer(InvadersFlyer):
    def __init__(self, shot_count=0):
        self._player_shot_count = shot_count
        maker = BitmapMaker.instance()
        self._map = maker.saucer
        self.mask = pygame.mask.from_surface(self._map)
        self.rect = self._map.get_rect()
        even_or_odd = shot_count % 2
        starting_x_coordinate = (u.INVADER_SAUCER_X_MAX, u.INVADER_SAUCER_X_MIN)[even_or_odd]
        self._speed = (-u.INVADER_SPEED, u.INVADER_SPEED)[even_or_odd]
        position = Vector2(starting_x_coordinate, u.INVADER_SAUCER_Y)
        self.position = position

Move the “starting” line down one and extract method.

class InvadersSaucer(InvadersFlyer):
    def __init__(self, shot_count=0):
        self._player_shot_count = shot_count
        maker = BitmapMaker.instance()
        self._map = maker.saucer
        self.mask = pygame.mask.from_surface(self._map)
        self.rect = self._map.get_rect()
        even_or_odd = shot_count % 2
        self._speed = (-u.INVADER_SPEED, u.INVADER_SPEED)[even_or_odd]
        position = self.starting_position(even_or_odd)
        self.position = position

Inline.

    def __init__(self, shot_count=0):
        self._player_shot_count = shot_count
        maker = BitmapMaker.instance()
        self._map = maker.saucer
        self.mask = pygame.mask.from_surface(self._map)
        self.rect = self._map.get_rect()
        even_or_odd = shot_count % 2
        self._speed = (-u.INVADER_SPEED, u.INVADER_SPEED)[even_or_odd]
        self.position = self.starting_position(even_or_odd)

Extract Method.

class InvadersSaucer(InvadersFlyer):
    def __init__(self, shot_count=0):
        self._player_shot_count = shot_count
        maker = BitmapMaker.instance()
        self._map = maker.saucer
        self.mask = pygame.mask.from_surface(self._map)
        self.rect = self._map.get_rect()
        self.init_position_and_speed(shot_count)

    def init_position_and_speed(self, shot_count):
        even_or_odd = shot_count % 2
        self._speed = (-u.INVADER_SPEED, u.INVADER_SPEED)[even_or_odd]
        self.position = self.starting_position(even_or_odd)

Extract Method.

class InvadersSaucer(InvadersFlyer):
    def __init__(self, shot_count=0):
        self.init_map_mask_rect(shot_count)
        self.init_position_and_speed(shot_count)

    # noinspection PyAttributeOutsideInit
    def init_map_mask_rect(self, shot_count):
        self._player_shot_count = shot_count
        maker = BitmapMaker.instance()
        self._map = maker.saucer
        self.mask = pygame.mask.from_surface(self._map)
        self.rect = self._map.get_rect()

    # noinspection PyAttributeOutsideInit
    def init_position_and_speed(self, shot_count):
        even_or_odd = shot_count % 2
        self._speed = (-u.INVADER_SPEED, u.INVADER_SPEED)[even_or_odd]
        self.position = self.starting_position(even_or_odd)

    def starting_position(self, even_or_odd):
        starting_x_coordinate = (u.INVADER_SAUCER_X_MAX, u.INVADER_SAUCER_X_MIN)[even_or_odd]
        position = Vector2(starting_x_coordinate, u.INVADER_SAUCER_Y)
        return position

I think I want to try inlining the starting position code to see if I like it better that way.

    # noinspection PyAttributeOutsideInit
    def init_position_and_speed(self, shot_count):
        even_or_odd = shot_count % 2
        starting_x_coordinate = (u.INVADER_SAUCER_X_MAX, u.INVADER_SAUCER_X_MIN)[even_or_odd]
        self.position = Vector2(starting_x_coordinate, u.INVADER_SAUCER_Y)
        self._speed = (-u.INVADER_SPEED, u.INVADER_SPEED)[even_or_odd]

I do prefer that. We should stop. This is the size of at least two, maybe three articles now. Let’s review and sum up.

Review

InvadersSaucer used to be excessively complicated by having two strategy classes that dealt with its being only partially initialized when it was created. We created a new simple object, InvadersSaucerMaker that collects the necessary additional information, and decides whether to create the saucer or not.

class InvadersSaucerMaker(InvadersFlyer):
    def __init__(self):
        self.shot_count = None
        self.invader_count = None
        return None

    def interact_with(self, other, fleets):
        other.interact_with_invaderssaucermaker(self, fleets)

    def begin_interactions(self, fleets):
        self.shot_count = None
        self.invader_count = None

    def interact_with_invaderfleet(self, invader_fleet, fleets):
        self.invader_count = invader_fleet.invader_count()

    def interact_with_invaderplayer(self, invader_player, fleets):
        self.shot_count = invader_player.shot_count

    def end_interactions(self, fleets):
        if self.shot_count is not None and self.invader_count is not None and self.invader_count >= 8:
            fleets.append(InvadersSaucer(self.shot_count))
        fleets.remove(self)
        new_maker = InvadersSaucerMaker()
        fleets.append(TimeCapsule(10, new_maker))

A few simple changes to InvadersSaucer let us do the full initialization at creation time, and we quickly had the game running in the new form. That eliminated the PreInitStrategy entirely.

Removing the PostInitStrategy was also quite simple, basically just inlining the methods that it called into the saucer code.

Summary

The articles are long, but the work was small, all in small steps, mostly pure refactorings. It’s just that I needed to show you a lot of code. (That’s my story and I’m sticking to it.)

What we have seen is significant, in that we have taken an object that was complicated and hard to understand, replaced it with two objects which are each easy to understand, and whose relationship is also quite simple: one creates the other.

When we break things up well, that’s what happens: simpler objects interacting simply. And, when we don’t get it right, we can feel it right away, because things get worse instead of better. Fortunately, we did not fall into that trap today.

The saucer is now a very standard object in accord with our design style, and the saucer maker is straightforward as well. I’ll include the code for both one more time, just to see if I can fill up the Internet.

See you next time!



class InvadersSaucerMaker(InvadersFlyer):
    def __init__(self):
        self.shot_count = None
        self.invader_count = None

    @property
    def mask(self):
        return None

    @property
    def rect(self):
        return None

    def interact_with(self, other, fleets):
        other.interact_with_invaderssaucermaker(self, fleets)

    def begin_interactions(self, fleets):
        self.shot_count = None
        self.invader_count = None

    def interact_with_invaderfleet(self, invader_fleet, fleets):
        self.invader_count = invader_fleet.invader_count()

    def interact_with_invaderplayer(self, invader_player, fleets):
        self.shot_count = invader_player.shot_count

    def end_interactions(self, fleets):
        if self.shot_count is not None and self.invader_count is not None and self.invader_count >= 8:
            fleets.append(InvadersSaucer(self.shot_count))
        fleets.remove(self)
        new_maker = InvadersSaucerMaker()
        fleets.append(TimeCapsule(10, new_maker))

class InvadersSaucer(InvadersFlyer):
    def __init__(self, shot_count=0):
        self.init_map_mask_rect(shot_count)
        self.init_position_and_speed(shot_count)

    # noinspection PyAttributeOutsideInit
    def init_map_mask_rect(self, shot_count):
        self._player_shot_count = shot_count
        maker = BitmapMaker.instance()
        self._map = maker.saucer
        self.mask = pygame.mask.from_surface(self._map)
        self.rect = self._map.get_rect()

    # noinspection PyAttributeOutsideInit
    def init_position_and_speed(self, shot_count):
        even_or_odd = shot_count % 2
        starting_x_coordinate = (u.INVADER_SAUCER_X_MAX, u.INVADER_SAUCER_X_MIN)[even_or_odd]
        self.position = Vector2(starting_x_coordinate, u.INVADER_SAUCER_Y)
        self._speed = (-u.INVADER_SPEED, u.INVADER_SPEED)[even_or_odd]

    @property
    def mask(self):
        return self._mask

    @mask.setter
    def mask(self, value):
        self._mask = value

    @property
    def rect(self):
        return self._rect

    @rect.setter
    def rect(self, value):
        self._rect = value

    @property
    def position(self):
        return Vector2(self.rect.center)

    @position.setter
    def position(self, value):
        self.rect.center = value

    def interact_with(self, other, fleets):
        other.interact_with_invaderssaucer(self, fleets)

    def interact_with_playershot(self, shot, fleets):
        if Collider(self, shot).colliding():
            explosion = InvadersExplosion.saucer_explosion(self.position, 0.5)
            fleets.append(explosion)
            fleets.append(InvaderScore(self._mystery_score()))
            self._die(fleets)

    def update(self, delta_time, fleets):
        self._move_along_x()
        self._adjust_stereo_position()
        self._die_if_done(fleets)

    def draw(self, screen):
        screen.blit(self._map, self.rect)

    def _die(self, fleets):
        fleets.remove(self)

    def _move_along_x(self):
        self.position = (self.position.x + self._speed, self.position.y)

    def _adjust_stereo_position(self):
        frac = (self.position.x - u.INVADER_SAUCER_X_MIN) / (u.INVADER_SAUCER_X_MAX - u.INVADER_SAUCER_X_MIN)
        player.play_stereo("ufo_lowpitch", frac, False)

    def _die_if_done(self, fleets):
        if self._going_off_screen():
            self._die(fleets)

    def _going_off_screen(self):
        return not u.INVADER_SAUCER_X_MIN <= self.position.x <= u.INVADER_SAUCER_X_MAX

    def _mystery_score(self):
        score_index = self._player_shot_count % len(u.INVADER_SAUCER_SCORE_LIST)
        return u.INVADER_SAUCER_SCORE_LIST[score_index]