Python Asteroids on GitHub

Let me do a little background TDD here, see what I come up with.

Begin with a test:

class TestHyperspaceGenerator:
    def test_exists(self):
        impossible = Vector2(-5, -9)
        ship = Ship(impossible)
        HyperspaceGenerator(ship)

The test runs, given this:

class HyperspaceGenerator:
    def __init__(self, ship):
        self._ship = ship

Small enough. I could have written a simpler test but I do think we want the thing to know the ship because it’s part of the ship and is plugged into it somehow. I don’t know how hyperspace generators actually work …

More test.

    def test_initializes_ready(self):
        impossible = Vector2(-5, -9)
        ship = Ship(impossible)
        hg = HyperspaceGenerator(ship)
        assert hg.can_try_hyperspace()

I could fake it til I make it. Let’s.

    def can_try_hyperspace(self):
        return True

More test. I want to remove the duplication first. First I make more duplication:


class TestHyperspaceGenerator:
    def test_exists(self):
        impossible = Vector2(-5, -9)
        ship = Ship(impossible)
        hg = HyperspaceGenerator(ship)

    def test_initializes_ready(self):
        impossible = Vector2(-5, -9)
        ship = Ship(impossible)
        hg = HyperspaceGenerator(ship)
        assert hg.can_try_hyperspace()

Extract. PyCharm is recalcitrant but we get there:

class TestHyperspaceGenerator:
    def test_exists(self):
        self.setup_generator()

    def setup_generator(self):
        impossible = Vector2(-5, -9)
        ship = Ship(impossible)
        return HyperspaceGenerator(ship)

    def test_initializes_ready(self):
        hg = self.setup_generator()
        assert hg.can_try_hyperspace()

Test is green. Let’s do another, though it’ll be much like the first two.

    def test_can_try_makes_unready(self):
        hg = self.setup_generator()
        assert hg.can_try_hyperspace()
        assert not hg.can_try_hyperspace()

This is red but won’t be for long.

class HyperspaceGenerator:
    def __init__(self, ship):
        self._ship = ship
        self._button_unpressed = True

    def can_try_hyperspace(self):
        if self._button_unpressed:
            self._button_unpressed = False
            return True
        else:
            return False

Test is green. Another:

    def test_lift_off_button(self):
        hg = self.setup_generator()
        assert hg.can_try_hyperspace()
        assert not hg.can_try_hyperspace()
        hg.lift_off_button()
        assert hg.can_try_hyperspace()

Fails of course, until:

    def lift_off_button(self):
        self._button_unpressed = True

More. What was that list of ideas in the previous article?

  1. HyperspaceGenerator has two internal flags, ready and recharged. It inits to ready and recharged.
  2. We create one in __init__.
  3. In enter_hyperspace_if_possible we ask it can_try_hyperspace.
  4. It answers True if both ready and recharge. I think it should reset ready right then.
  5. When the key goes up, we tell it set_ready.
  6. If we go to hyperspace, we tell it trigger_hyperspace, and it resets recharged.
  7. When we tick, we send tick to the generator. It ticks as needed and resets recharged as needed.

I like those names better. Rename in HG.

class HyperspaceGenerator:
    def __init__(self, ship):
        self._ship = ship
        self._ready = True

    def can_try_hyperspace(self):
        if self._ready:
            self._ready = False
            return True
        else:
            return False

    def lift_off_button(self):
        self._ready = True

I like rename a lot.

The list suggests set_ready but I like lift_off_button better. It’s the event the HG would see.

Now trigger:

    def test_trigger(self):
        impossible = Vector2(-5, -9)
        hg = self.setup_generator()
        if hg.can_try_hyperspace():
            hg.enter_hyperspace()
            assert hg._ship.position != impossible

I don’t like my setup as much as I had hoped but we’re OK I think at least until we get to green again.

Now a dilemma. What if someone asked can_try_hyperspace, got a no and tried to enter anyway? How would we know. I think we cannot reset the flag on can. Grr. Change the other test.

No, maybe we should leave that alone and have more states.

I am not loving this. Fortunately this is a spike. I’ll leave the two preceding tests alone and try making this one run. I think we may need different logic than I predicted.

Path too rocky. Back up and try another way.

After just a little thought, I think we want to change how this thing works. I propose that it should be like this.

  1. HyperspaceGenerator can be charged or discharged. It starts … maybe discharged, so you can’t go to hyperspace right away.
  2. When you press the button, if charged, HG will attempt to go to hyperspace. Details below.
  3. HG will see button_pressed multiple times, since you’ll be holding it down. It must only attempt to go in once per press.
  4. On press when discharged, HG will not go to hyperspace and will wait for button_lifted before trying again.
  5. When discharged, HG will charge until the charge timer runs down.
  6. On press after button_pressed and no lift, nothing happens.

I think that’s closer. The big changes are that we are thinking in terms of user actions, which are only press_button and lift_button or whatever we name them. You must lift after each press and a press only tries to go to hyperspace when you press and it is charged.

I’ll just roll back and start over. No real value in trying to mash these tests, better to work on the new ideas.

class TestHyperspaceGenerator:
    def test_exists(self):
        ship = Ship(Vector2(0, 0))
        hg = HyperspaceGenerator(ship)

class HyperspaceGenerator:
    def __init__(self, ship):
        self._ship = ship

Passes. Test discharged.

    def test_starts_discharged(self):
        ship = Ship(Vector2(0, 0))
        hg = HyperspaceGenerator(ship)
        assert not hg._charged

class HyperspaceGenerator:
    def __init__(self, ship):
        self._charged = False
        self._ship = ship

Test went to hyperspace. This test will probably not endure.

    def test_enters_hyperspace(self):
        impossible = Vector2(-5, -9)
        ship = Ship(impossible)
        hg = HyperspaceGenerator(ship)
        hg.press_button()
        assert ship.position != impossible

class HyperspaceGenerator:
    def press_button(self):
        pass

We need some work here. I feel that this press_button method may not be robust enough.

I guess there are two cases. Either we explode or we jump to a new location.

Let’s assume the best for this test. I rip the logic out of Ship:

    def press_button(self):
        self.hyperspace_jump()

    def hyperspace_jump(self):
        x = random.randrange(u.SCREEN_SIZE)
        y = random.randrange(u.SCREEN_SIZE)
        self._ship.move_to(Vector2(x, y))
        self._ship._angle = random.randrange(360)
        dx = random.randrange(u.SHIP_HYPERSPACE_MAX_VELOCITY)
        dy = random.randrange(u.SHIP_HYPERSPACE_MAX_VELOCITY)
        self._ship.accelerate_to(Vector2(dx, dy))

Test passes. I am being distracted by cats and people. This may get ragged. Still just a spike, I’ll do my best.

What next? Can’t go again, until you lift your finger. What about charging? Let’s do that first.

    def test_next_entry_requires_recharge(self):
        impossible = Vector2(-5, -9)
        ship = Ship(impossible)
        hg = HyperspaceGenerator(ship)
        hg.press_button()
        ship.move_to(impossible)
        hg.press_button()
        assert ship.position == impossible
        hg.recharge()
        hg.press_button()
        assert ship.position != impossible

THis is failing at the == impossible, because it went to hyperspace again.

    def test_next_entry_requires_recharge(self):
        impossible = Vector2(-5, -9)
        ship = Ship(impossible)
        hg = HyperspaceGenerator(ship)
        hg.press_button()
        ship.move_to(impossible)
        hg.press_button()
>       assert ship.position == impossible
E       assert <Vector2(470, 222)> == <Vector2(-5, -9)>

Let’s hammer something in.

class HyperspaceGenerator:
    def __init__(self, ship):
        self._charged = False
        self._ship = ship

    def press_button(self):
        if self._charged:
            self.hyperspace_jump()

    def hyperspace_jump(self):
        self._charged = False
        x = random.randrange(u.SCREEN_SIZE)
        y = random.randrange(u.SCREEN_SIZE)
        self._ship.move_to(Vector2(x, y))
        self._ship._angle = random.randrange(360)
        dx = random.randrange(u.SHIP_HYPERSPACE_MAX_VELOCITY)
        dy = random.randrange(u.SHIP_HYPERSPACE_MAX_VELOCITY)
        self._ship.accelerate_to(Vector2(dx, dy))

    def recharge(self):
        self._charged = True

This breaks an old test, because we are initializing as not charged. Change that test:

    def test_enters_hyperspace(self):
        impossible = Vector2(-5, -9)
        ship = Ship(impossible)
        hg = HyperspaceGenerator(ship)
        hg.recharge(). # <=== added
        hg.press_button()
        assert ship.position != impossible

Green. I think this is going well enough to commit it, and the pizza will be here any minute. Commit: HyperspaceGenerator and tests in process.

Summary

It’s a decent start on a new HyperspaceGenerator object. It’s also almost time for pizza and then, later, the FGNOzZE1. I plan to peck away at this a bit more and I’ll report tomorrow. See you then!



  1. Friday Geeks’ Night Out and Zoom Ensemble, held every Tuesday evening except when it isn’t.