Python Asteroids on GitHub

I’m going to do the tiny object for the nearest code. I just want to see how it looks. Talk about gilding the lily.

We’ll TDD it, I think, copying the existing nearest tests. New class name: Targeter.

It will just handle a single coordinate, either x or y: that will be enough to try out what I’m interested in. Here’s the first cut, passing its tests:

class Targeter:
    def __init__(self, shooter_coordinate, target_coordinate, screen_size):
        self.to_coordinate = target_coordinate
        self.from_coordinate = shooter_coordinate
        self.size = screen_size
        self.half = self.size / 2

    def nearest(self):
        direct_distance = abs(self.to_coordinate - self.from_coordinate)
        if direct_distance <= self.half:
            return self.to_coordinate
        elif self.from_coordinate >= self.half:
            return self.to_coordinate + self.size
        else:
            return self.to_coordinate - self.size

What I wanted to try was some Extract Method action, hoping for more clarity. This may take a few tries on the names.

OK. Here’s the revised nearest method from my new object:

    def nearest(self):
        if self.target_is_near:
            return self.target_coordinate
        elif self.we_are_past_center:
            return self.target_on_high_side
        else:
            return self.target_on_low_side

Now it seems to me that that’s about as short and clear as it could be. Of course, we wonder a bit about how it got that way. But first, here’s the test, so you can see how we use this thing:

    def test_can_choose_nearest_scalar_target(self):
        target = 100
        screen_size = 500
        shooter = 200
        assert Targeter(shooter, target, screen_size).nearest() == 100
        shooter = 400
        assert Targeter(shooter, target, screen_size).nearest() == 100 + 500
        target = 400
        shooter = 100
        assert Targeter(shooter, target, screen_size).nearest() == 400 - 500

Same numbers, new notation.

How did we do it? Methods, converted to properties:

class Targeter:
    def __init__(self, shooter_coordinate, target_coordinate, screen_size):
        self.target_coordinate = target_coordinate
        self.from_coordinate = shooter_coordinate
        self.size = screen_size
        self.half = self.size / 2

    def nearest(self):
        if self.target_is_near:
            return self.target_coordinate
        elif self.we_are_past_center:
            return self.target_on_high_side
        else:
            return self.target_on_low_side

    @property
    def target_is_near(self):
        return self.direct_distance <= self.half

    @property
    def direct_distance(self):
        return abs(self.target_coordinate - self.from_coordinate)

    @property
    def we_are_past_center(self):
        return self.from_coordinate >= self.half

    @property
    def target_on_high_side(self):
        return self.target_coordinate + self.size

    @property
    def target_on_low_side(self):
        return self.target_coordinate - self.size

Would I ever go that far? Well, in Python, I might not. In Smalltalk, I could imagine it, because Smalltalkers just love lots of tiny methods.

We might prefer a simpler calling sequence, like this:

    def test_best_target(self):
        target = 100
        screen_size = 500
        shooter = 200
        assert Targeter.best_target(shooter, target, screen_size) == 100
        shooter = 400
        assert Targeter.best_target(shooter, target, screen_size) == 100 + 500
        target = 400
        shooter = 100
        assert Targeter.best_target(shooter, target, screen_size) == 400 - 500

That’s easily done:

class Targeter:

    @classmethod
    def best_target(cls, shooter_coordinate, target_coordinate, screen_size):
        return Targeter(shooter_coordinate, target_coordinate, screen_size).nearest()

We could fiddle with the names. But that would be silly, wouldn’t it?

All I really wanted to do was to get to this method, which I personally feel is very nice but almost certainly over-polished:

    def nearest(self):
        if self.target_is_near:
            return self.target_coordinate
        elif self.we_are_past_center:
            return self.target_on_high_side
        else:
            return self.target_on_low_side

I don’t know whether to be proud or ashamed. Maybe both!

Will I plug it in? Very possibly. But, Ron! It’s certainly less efficient with all those method calls! Right, every two seconds or whatever, when we fire a targeted missile. Do I care about something that is microseconds slower every couple of seconds? Probably not.

See you next time!