Python Asteroids on GitHub

I begin a long refactoring session. I’ll split the article another time or two. I am a merciful person at heart.

Second in Series

Here, without committing the solution from the preceding article (I should have, but I didn’t) I begin refactoring the solution to avoid the temporal coupling and to generally improve the implementation. It’s long, smooth, and all goes well.

Lots of nice small steps in here. Browse a bit, you might find it interesting.

Moving Along

So let’s make more of a mess. My tentative plan, so far, is to keep inlining methods until I get everything I need in one place, and then to rearrange things in a more felicitous, nicer, and generally better fashion.

This time, with no real plan, I’m think I’m going to do that inlining fairly blindly, expecting that a plan will emerge. At that point, we might roll back and do it over, roll back and try something new, or continue, improving the code until we actually like it. That last case would be my favorite, but I grant that it’s not the mostly likely. More likely is that we’ll see some smaller changes, and do it again. I hope the least likely is that I’ll throw up my hands and scream “Nooooo!””

But … maybe we can do better than blind inlining. I think I see a tempting opportunity.

One thing that strikes me as odd about all this is that we call create_targeted_missile in two separate states. In the one case, we have actually chosen a random target and have set the adjustment ratio to one, using an un-adjusted velocity. In the other, we have a real target and want the adjusted value. But the code wanders down to the same place.

That starts happening here:

    def fire_available_missile(self, chance, fleets, saucer, ship_or_none):
        self._adjustment_ratio = 1
        ship_position = self.select_aiming_point(chance, saucer, ship_or_none)
        self.create_targeted_missile(saucer.position, ship_position, fleets)

    def select_aiming_point(self, chance, saucer, ship_or_none):
        if not ship_or_none:
            return self.random_position()
        elif saucer.always_target:
            return self.optimal_aiming_point(saucer, ship_or_none)
        elif chance < u.SAUCER_TARGETING_FRACTION:
            return self.optimal_aiming_point(saucer, ship_or_none)
        else:
            return self.random_position()

Could we have two methods, perhaps create_optimized_missile and create_unoptimized_missile, and use whichever one we mean?

Note
Nice refactoring sequence starting here.

Let’s inline the thing above and see if we can get there.

    def fire_available_missile(self, chance, fleets, saucer, ship_or_none):
        self._adjustment_ratio = 1
        if not ship_or_none:
            result = self.random_position()
        elif saucer.always_target:
            result = self.optimal_aiming_point(saucer, ship_or_none)
        elif chance < u.SAUCER_TARGETING_FRACTION:
            result = self.optimal_aiming_point(saucer, ship_or_none)
        else:
            result = self.random_position()
        ship_position = result
        self.create_targeted_missile(saucer.position, ship_position, fleets)

Duplicate the create call up into each branch. No first, inline ship_position in the final call.

    def fire_available_missile(self, chance, fleets, saucer, ship_or_none):
        self._adjustment_ratio = 1
        if not ship_or_none:
            result = self.random_position()
        elif saucer.always_target:
            result = self.optimal_aiming_point(saucer, ship_or_none)
        elif chance < u.SAUCER_TARGETING_FRACTION:
            result = self.optimal_aiming_point(saucer, ship_or_none)
        else:
            result = self.random_position()
        self.create_targeted_missile(saucer.position, result, fleets)

Now duplicate the bottom line and move up and into the ifs.

    def fire_available_missile(self, chance, fleets, saucer, ship_or_none):
        self._adjustment_ratio = 1
        if not ship_or_none:
            result = self.random_position()
            self.create_targeted_missile(saucer.position, result, fleets)
        elif saucer.always_target:
            result = self.optimal_aiming_point(saucer, ship_or_none)
            self.create_targeted_missile(saucer.position, result, fleets)
        elif chance < u.SAUCER_TARGETING_FRACTION:
            result = self.optimal_aiming_point(saucer, ship_or_none)
            self.create_targeted_missile(saucer.position, result, fleets)
        else:
            result = self.random_position()
            self.create_targeted_missile(saucer.position, result, fleets)

Let’s rename those result guys to target in the interest of clarity.

    def fire_available_missile(self, chance, fleets, saucer, ship_or_none):
        self._adjustment_ratio = 1
        if not ship_or_none:
            target = self.random_position()
            self.create_targeted_missile(saucer.position, target, fleets)
        elif saucer.always_target:
            target = self.optimal_aiming_point(saucer, ship_or_none)
            self.create_targeted_missile(saucer.position, target, fleets)
        elif chance < u.SAUCER_TARGETING_FRACTION:
            target = self.optimal_aiming_point(saucer, ship_or_none)
            self.create_targeted_missile(saucer.position, target, fleets)
        else:
            target = self.random_position()
            self.create_targeted_missile(saucer.position, target, fleets)

Yes. As an aside, I think I’d prefer that the target be first in those create calls, but we’ll let that ride for now.

Can we simplify the if nest while we’re here? Or should we just create our two methods and call them?

As I look at this code and imagine what it might do, I think I might do well to create a new method to use in the two actual targeting calls. Something like create_optimal_missile. I’m thinking that the new method would call the optimal_aiming_point method itself.

What happens if I extract those two lines to a new method?

    def fire_available_missile(self, chance, fleets, saucer, ship_or_none):
        self._adjustment_ratio = 1
        if not ship_or_none:
            target = self.random_position()
            self.create_targeted_missile(saucer.position, target, fleets)
        elif saucer.always_target:
            self.create_optimal_missile(fleets, saucer, ship_or_none)
        elif chance < u.SAUCER_TARGETING_FRACTION:
            self.create_optimal_missile(fleets, saucer, ship_or_none)
        else:
            target = self.random_position()
            self.create_targeted_missile(saucer.position, target, fleets)

    def create_optimal_missile(self, fleets, saucer, ship_or_none):
        target = self.optimal_aiming_point(saucer, ship_or_none)
        self.create_targeted_missile(saucer.position, target, fleets)

Now let’s inline the code from create_targeted_missile:

    def create_optimal_missile(self, fleets, saucer, ship_or_none):
        target = self.optimal_aiming_point(saucer, ship_or_none)
        vector_to_target = target - saucer.position
        direction_to_target = vector_to_target.normalize()
        missile_velocity = u.MISSILE_SPEED * direction_to_target
        adjusted_velocity = missile_velocity * self._adjustment_ratio
        offset = 2 * self._radius * direction_to_target
        missile = Missile.from_saucer(saucer.position + offset, adjusted_velocity)
        fleets.append(missile)
Note
The previous changes created an opportunity. I was tempted to wait to reap the benefit but decided to go ahead:

Now … I’m not going to do this, but now, create_targeted_missile can be renamed and simplified. No, in fact let’s do it now.

    def create_unoptimized_missile(self, from_position, to_position, fleets):
        vector_to_target = to_position - from_position
        direction_to_target = vector_to_target.normalize()
        missile_velocity = u.MISSILE_SPEED * direction_to_target
        offset = 2 * self._radius * direction_to_target
        missile = Missile.from_saucer(from_position + offset, missile_velocity)
        fleets.append(missile)

All our tests are green through all this, by the way. And I am confident that the game is good as well.

Now back to the optimal guy:

    def create_optimal_missile(self, fleets, saucer, ship_or_none):
        target = self.optimal_aiming_point(saucer, ship_or_none)
        vector_to_target = target - saucer.position
        direction_to_target = vector_to_target.normalize()
        missile_velocity = u.MISSILE_SPEED * direction_to_target
        adjusted_velocity = missile_velocity * self._adjustment_ratio
        offset = 2 * self._radius * direction_to_target
        missile = Missile.from_saucer(saucer.position + offset, adjusted_velocity)
        fleets.append(missile)
Note
Still doing small refactoring steps.

Let’s inline the optimal_aiming_point code.

    def create_optimal_missile(self, fleets, saucer, ship_or_none):
        target_position = self.closest_aiming_point(saucer.position, ship_or_none.position, u.SCREEN_SIZE)
        delta_position = target_position - saucer.position
        aim_time = self.time_to_target(delta_position, ship_or_none.velocity)
        # egregious hack
        self.set_adjustment_ratio(aim_time)
        target = target_position + ship_or_none.velocity * aim_time
        vector_to_target = target - saucer.position
        direction_to_target = vector_to_target.normalize()
        missile_velocity = u.MISSILE_SPEED * direction_to_target
        adjusted_velocity = missile_velocity * self._adjustment_ratio
        offset = 2 * self._radius * direction_to_target
        missile = Missile.from_saucer(saucer.position + offset, adjusted_velocity)
        fleets.append(missile)

We’re doing this with machine refactoring, so we don’t really even try to understand much … yet. We’re just trying to get a level playing field. Let’s inline the set_adjustment_ratio.

PyCharm declines to do this for me. Let me hand-refactor the little method.

Note
I could have done this by hand, but getting PyCharm to do it is safer. So I make a small change by hand and then let PyCharm go at it.

After refactoring by hand:

    def set_adjustment_ratio(self, aim_time):
        if aim_time:
            distance_to_target = aim_time*u.MISSILE_SPEED
            adjusted_distance = distance_to_target - 2*self._radius
            self._adjustment_ratio = adjusted_distance / distance_to_target

Now will PyCharm inline it for me? It will and we get this:

    def create_optimal_missile(self, fleets, saucer, ship_or_none):
        target_position = self.closest_aiming_point(saucer.position, ship_or_none.position, u.SCREEN_SIZE)
        delta_position = target_position - saucer.position
        aim_time = self.time_to_target(delta_position, ship_or_none.velocity)
        # egregious hack
        if aim_time:
            distance_to_target = aim_time * u.MISSILE_SPEED
            adjusted_distance = distance_to_target - 2 * self._radius
            self._adjustment_ratio = adjusted_distance / distance_to_target
        target = target_position + ship_or_none.velocity * aim_time
        vector_to_target = target - saucer.position
        direction_to_target = vector_to_target.normalize()
        missile_velocity = u.MISSILE_SPEED * direction_to_target
        adjusted_velocity = missile_velocity * self._adjustment_ratio
        offset = 2 * self._radius * direction_to_target
        missile = Missile.from_saucer(saucer.position + offset, adjusted_velocity)
        fleets.append(missile)

That is quite nasty. It is also green and I think it’s good.

Now some judicious non-machine editing on that if.

    def create_optimal_missile(self, fleets, saucer, ship_or_none):
        target_position = self.closest_aiming_point(saucer.position, ship_or_none.position, u.SCREEN_SIZE)
        delta_position = target_position - saucer.position
        aim_time = self.time_to_target(delta_position, ship_or_none.velocity)
        if aim_time:
            distance_to_target = aim_time * u.MISSILE_SPEED
            adjusted_distance = distance_to_target - 2 * self._radius
            adjustment_ratio = adjusted_distance / distance_to_target
        else:
            adjustment_ratio = 1
        target = target_position + ship_or_none.velocity * aim_time
        vector_to_target = target - saucer.position
        direction_to_target = vector_to_target.normalize()
        missile_velocity = u.MISSILE_SPEED * direction_to_target
        adjusted_velocity = missile_velocity * adjustment_ratio
        offset = 2 * self._radius * direction_to_target
        missile = Missile.from_saucer(saucer.position + offset, adjusted_velocity)
        fleets.append(missile)

I have now removed all uses of the spooky _adjustment_ratio member variable!! Remove any vestiges. Two setters setting it to 1, no uses in live code.

Note
I could have been, and probably should have been, committing these changes. I didn’t because I was making the code generally worse and was expecting that it would get better. In general, I would not like to leave the GitHub code “worse”. And, after all, this sequence might not have worked. I was confident but not deranged.

Now I want to compare my two creation methods.

    def create_optimal_missile(self, fleets, saucer, ship_or_none):
        target_position = self.closest_aiming_point(saucer.position, ship_or_none.position, u.SCREEN_SIZE)
        delta_position = target_position - saucer.position
        aim_time = self.time_to_target(delta_position, ship_or_none.velocity)
        if aim_time:
            distance_to_target = aim_time * u.MISSILE_SPEED
            adjusted_distance = distance_to_target - 2 * self._radius
            adjustment_ratio = adjusted_distance / distance_to_target
        else:
            adjustment_ratio = 1
        target = target_position + ship_or_none.velocity * aim_time

        vector_to_target = target - saucer.position
        direction_to_target = vector_to_target.normalize()
        missile_velocity = u.MISSILE_SPEED * direction_to_target
        adjusted_velocity = missile_velocity * adjustment_ratio
        offset = 2 * self._radius * direction_to_target
        missile = Missile.from_saucer(saucer.position + offset, adjusted_velocity)
        fleets.append(missile)

    def create_unoptimized_missile(self, from_position, to_position, fleets):
        vector_to_target = to_position - from_position
        direction_to_target = vector_to_target.normalize()
        missile_velocity = u.MISSILE_SPEED * direction_to_target
        offset = 2 * self._radius * direction_to_target
        missile = Missile.from_saucer(from_position + offset, missile_velocity)
        fleets.append(missile)

I added a blank line there to show the almost common code. If we were to add a parameter to create_unoptimized_missile, the adjustment factor, we could call it from our create optimal code. That would simplify create_optimal_missile a bit.

Note
My first thought was to rewrite the unoptimized method to take the parameter. It came to me that I could extract the parameterized one from existing code and then see whether I could better combine the two mostly-duplicated methods. That worked out perfectly.

First time through, I didn’t make quite the right extraction.

Let’s do this: we’ll extract the more powerful case from create_optimal_missile, then use it from the other.

    def create_optimal_missile(self, fleets, saucer, ship_or_none):
        target_position = self.closest_aiming_point(saucer.position, ship_or_none.position, u.SCREEN_SIZE)
        delta_position = target_position - saucer.position
        aim_time = self.time_to_target(delta_position, ship_or_none.velocity)
        if aim_time:
            distance_to_target = aim_time * u.MISSILE_SPEED
            adjusted_distance = distance_to_target - 2 * self._radius
            adjustment_ratio = adjusted_distance / distance_to_target
        else:
            adjustment_ratio = 1
        target = target_position + ship_or_none.velocity * aim_time
        self.create_adjusted_missile(adjustment_ratio, target, saucer, fleets)

    def create_adjusted_missile(self, adjustment_ratio, target, saucer, fleets):
        vector_to_target = target - saucer.position
        direction_to_target = vector_to_target.normalize()
        missile_velocity = u.MISSILE_SPEED * direction_to_target
        adjusted_velocity = missile_velocity * adjustment_ratio
        offset = 2 * self._radius * direction_to_target
        missile = Missile.from_saucer(saucer.position + offset, adjusted_velocity)
        fleets.append(missile)

No. That won’t quite do. Our unoptimized method doesn’t have the saucer. Undo the extract and do again. We’re here:

    def create_optimal_missile(self, fleets, saucer, ship_or_none):
        target_position = self.closest_aiming_point(saucer.position, ship_or_none.position, u.SCREEN_SIZE)
        delta_position = target_position - saucer.position
        aim_time = self.time_to_target(delta_position, ship_or_none.velocity)
        if aim_time:
            distance_to_target = aim_time * u.MISSILE_SPEED
            adjusted_distance = distance_to_target - 2 * self._radius
            adjustment_ratio = adjusted_distance / distance_to_target
        else:
            adjustment_ratio = 1
        target = target_position + ship_or_none.velocity * aim_time

        vector_to_target = target - saucer.position
        direction_to_target = vector_to_target.normalize()
        missile_velocity = u.MISSILE_SPEED * direction_to_target
        adjusted_velocity = missile_velocity * adjustment_ratio
        offset = 2 * self._radius * direction_to_target
        missile = Missile.from_saucer(saucer.position + offset, adjusted_velocity)
        fleets.append(missile)
Note
Having done the extract, we take the opportunity to improve things a bit.

Extract variable:

    def create_optimal_missile(self, fleets, saucer, ship_or_none):
        target_position = self.closest_aiming_point(saucer.position, ship_or_none.position, u.SCREEN_SIZE)
        delta_position = target_position - saucer.position
        aim_time = self.time_to_target(delta_position, ship_or_none.velocity)
        if aim_time:
            distance_to_target = aim_time * u.MISSILE_SPEED
            adjusted_distance = distance_to_target - 2 * self._radius
            adjustment_ratio = adjusted_distance / distance_to_target
        else:
            adjustment_ratio = 1
        target = target_position + ship_or_none.velocity * aim_time

        saucer_position = saucer.position
        vector_to_target = target - saucer_position
        direction_to_target = vector_to_target.normalize()
        missile_velocity = u.MISSILE_SPEED * direction_to_target
        adjusted_velocity = missile_velocity * adjustment_ratio
        offset = 2 * self._radius * direction_to_target
        missile = Missile.from_saucer(saucer_position + offset, adjusted_velocity)
        fleets.append(missile)

Extract method again.

    def create_optimal_missile(self, fleets, saucer, ship_or_none):
        target_position = self.closest_aiming_point(saucer.position, ship_or_none.position, u.SCREEN_SIZE)
        delta_position = target_position - saucer.position
        aim_time = self.time_to_target(delta_position, ship_or_none.velocity)
        if aim_time:
            distance_to_target = aim_time * u.MISSILE_SPEED
            adjusted_distance = distance_to_target - 2 * self._radius
            adjustment_ratio = adjusted_distance / distance_to_target
        else:
            adjustment_ratio = 1
        target = target_position + ship_or_none.velocity * aim_time

        saucer_position = saucer.position
        self.create_adjusted_missile(adjustment_ratio, target, saucer_position, fleets)

    def create_adjusted_missile(self, adjustment_ratio, target_position, saucer_position, fleets):
        vector_to_target = target_position - saucer_position
        direction_to_target = vector_to_target.normalize()
        missile_velocity = u.MISSILE_SPEED * direction_to_target
        adjusted_velocity = missile_velocity * adjustment_ratio
        offset = 2 * self._radius * direction_to_target
        missile = Missile.from_saucer(saucer_position + offset, adjusted_velocity)
        fleets.append(missile)

And now I think I can do this:

    def create_unoptimized_missile(self, from_position, to_position, fleets):
        self.create_adjusted_missile(1, to_position, from_position, fleets)
Note
Extracting the more powerful method worked out perfectly: it allowed us to replace all the code of the simplified one with a call to the more powerful one. I think that worked out very nicely.

I think this code is improved enough to commit. Tests are green and I think it’s better if not great.

Commit: refactoring Gunner. Flaky adjustment member removed.

Note
This has been so stress-free that I am not even slightly tired at this point. So I decide to continue refactoring for improved clarity.

We’ll split the article here, since there’s an actual commit. More refactoring in the next article, all from the same morning session.

Next in series