Refactor Saucer
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]