Python Asteroids on GitHub

I want to take a look at the explosion, especially the draw method and see how we can clean it up. Many many tiny steps FTW!

There are a couple of issues with the Fragment drawing. One is that the GFragment drawing is rather messy. Another is that both GFragment and VFragment go though all the Fragment init and then don’t use it. Each of them computes its drawing information in draw instead of once upon creation. We can probably do better.

Let’s review the code.

class Fragment:
    def __init__(self, position, angle=None, speed_mul=None):
        angle = angle if angle is not None else random.randrange(360)
        self.position = position
        half_length = random.uniform(6, 10)
        self.begin = Vector2(-half_length, 0)
        self.end = Vector2(half_length, 0)
        speed_mul = speed_mul if speed_mul is not None else random.uniform(0.25, 0.5)
        self.velocity = speed_mul*Vector2(u.FRAGMENT_SPEED, 0).rotate(angle)
        self.theta = random.randrange(0, 360)
        self.delta_theta = random.uniform(180, 360)*random.choice((1, -1))
        self.timer = Timer(u.FRAGMENT_LIFETIME, self.timeout)

    def draw(self, screen):
        begin = self.position + self.begin.rotate(self.theta)
        end = self.position + self.end.rotate(self.theta)
        pygame.draw.line(screen, "white", begin, end, 3)

class VFragment(Fragment):
    def __init__(self, position, angle=None, speed_mul=None):
        super().__init__(position, angle, speed_mul)

    def draw(self, screen):
        v_shape = [Vector2(-7, 5), Vector2(7, 0), Vector2(-7, -5)]
        points = [p.rotate(self.theta) + self.position for p in v_shape]
        pygame.draw.lines(screen, "white", False, points, 3)

class GFragment(Fragment):
    def __init__(self, position, angle=None, speed_mul=None):
        super().__init__(position, angle, speed_mul)

    def draw(self, screen):
        theta = self.theta
        head_off = Vector2(0,16+8).rotate(theta)
        position = self.position
        pygame.draw.circle(screen, "white", position + head_off, 8, 2)
        body_top = Vector2(0, 16).rotate(theta)
        body_bottom = Vector2(0, 2).rotate(theta)
        pygame.draw.line(screen, "white", body_top + position, body_bottom + position, 3)
        leg_left = Vector2(-5, -16).rotate(theta)
        pygame.draw.line(screen, "white", leg_left + position, body_bottom + position, 3)
        leg_right = Vector2(5, -16).rotate(theta)
        pygame.draw.line(screen, "white", leg_right + position, body_bottom + position, 3)
        arm_left = Vector2(-9, 10).rotate(theta)
        arm_right = Vector2(9, 10).rotate(theta)
        pygame.draw.line(screen, "white", arm_right + position, arm_left + position, 3)

In the Fragment we have just two points, we rotate them and add in position and draw the line. In the GFragment, we have four pairs of points that we rotate and draw lines. In the VFragment, we have three points and use a different function, but we could make that two pairs and draw two lines.

So it seems to me that we have something like “given a collection of pairs of points, rotate the points, add position, and draw lines between each pair.”

Take a look at just this bit:

        arm_left = Vector2(-9, 10).rotate(theta)
        arm_right = Vector2(9, 10).rotate(theta)
        pygame.draw.line(screen, "white", arm_right + position, arm_left + position, 3)

Let’s extract two variables:

        arm_left_raw = Vector2(-9, 10)
        arm_left = arm_left_raw.rotate(theta)
        arm_right_raw = Vector2(9, 10)
        arm_right = arm_right_raw.rotate(theta)
        pygame.draw.line(screen, "white", arm_right + position, arm_left + position, 3)

Now move a line:

        arm_left_raw = Vector2(-9, 10)
        arm_right_raw = Vector2(9, 10)
        arm_left = arm_left_raw.rotate(theta)
        arm_right = arm_right_raw.rotate(theta)
        pygame.draw.line(screen, "white", arm_right + position, arm_left + position, 3)

Extract method:

        arm_left_raw = Vector2(-9, 10)
        arm_right_raw = Vector2(9, 10)
        self.draw_one_line(arm_left_raw, arm_right_raw, position, screen, theta)

    def draw_one_line(self, arm_left_raw, arm_right_raw, position, screen, theta):
        arm_left = arm_left_raw.rotate(theta)
        arm_right = arm_right_raw.rotate(theta)
        pygame.draw.line(screen, "white", arm_right + position, arm_left + position, 3)

Rename the parameters:

    def draw_one_line(self, line_start, line_end, position, screen, theta):
        arm_left = line_start.rotate(theta)
        arm_right = line_end.rotate(theta)
        pygame.draw.line(screen, "white", arm_right + position, arm_left + position, 3)

I could be committing this after each step. Let’s do: refactoring Fragment draw code.

Rename temps:

    def draw_one_line(self, line_start, line_end, position, screen, theta):
        start = line_start.rotate(theta)
        end = line_end.rotate(theta)
        pygame.draw.line(screen, "white", end + position, start + position, 3)

Commit same message. Inline start and end.

    def draw_one_line(self, line_start, line_end, position, screen, theta):
        pygame.draw.line(screen, "white", line_end.rotate(theta) + position, line_start.rotate(theta) + position, 3)

Notice that we’re drawing from end to start, resolve to fix that. Extract the full expressions that we inlined:

    def draw_one_line(self, line_start, line_end, position, screen, theta):
        start = line_end.rotate(theta) + position
        end = line_start.rotate(theta) + position
        pygame.draw.line(screen, "white", start, end, 3)

I call that nice. Commit: new function draw_one_line.

Now, of course, I want to apply this everywhere. Let’s review the whole draw method again:

    def draw(self, screen):
        theta = self.theta
        head_off = Vector2(0,16+8).rotate(theta)
        position = self.position
        pygame.draw.circle(screen, "white", position + head_off, 8, 2)
        body_top = Vector2(0, 16).rotate(theta)
        body_bottom = Vector2(0, 2).rotate(theta)
        pygame.draw.line(screen, "white", body_top + position, body_bottom + position, 3)
        leg_left = Vector2(-5, -16).rotate(theta)
        pygame.draw.line(screen, "white", leg_left + position, body_bottom + position, 3)
        leg_right = Vector2(5, -16).rotate(theta)
        pygame.draw.line(screen, "white", leg_right + position, body_bottom + position, 3)
        arm_left_raw = Vector2(-9, 10)
        arm_right_raw = Vector2(9, 10)
        self.draw_one_line(arm_left_raw, arm_right_raw, position, screen, theta)

Meh. What I really want (what I really really want) is to pass in a pair of vectors at a time rather than two individuals. OK, let’s do that by hand, I don’t see a mechanical step.

        arm_left_raw = Vector2(-9, 10)
        arm_right_raw = Vector2(9, 10)
        pair = [arm_left_raw, arm_right_raw]
        self.draw_one_line(pair, position, screen, theta)

    def draw_one_line(self, pair, position, screen, theta):
        start = pair[0].rotate(theta) + position
        end = pair[1].rotate(theta) + position
        pygame.draw.line(screen, "white", start, end, 3)

Commit: convert draw_one_line to accept pair of Vector2.

Inline:

        pair = [(Vector2(-9, 10)), (Vector2(9, 10))]
        self.draw_one_line(pair, position, screen, theta)

    def draw_one_line(self, pair, position, screen, theta):
        start = pair[0].rotate(theta) + position
        end = pair[1].rotate(theta) + position
        pygame.draw.line(screen, "white", start, end, 3)

Commit: inline pair creation.

Now I’d like to use my function in a loop. So maybe now is the time to do that.

(I should mention that I’m just following my nose here, I have no grand plan, and just now I don’t see a series of mechanical refactoring steps to do the job.)

    def draw(self, screen):
        theta = self.theta
        head_off = Vector2(0,16+8).rotate(theta)
        position = self.position
        pygame.draw.circle(screen, "white", position + head_off, 8, 2)
        body_top = Vector2(0, 16).rotate(theta)
        body_bottom = Vector2(0, 2).rotate(theta)
        pygame.draw.line(screen, "white", body_top + position, body_bottom + position, 3)
        leg_left = Vector2(-5, -16).rotate(theta)
        pygame.draw.line(screen, "white", leg_left + position, body_bottom + position, 3)
        leg_right = Vector2(5, -16).rotate(theta)
        pygame.draw.line(screen, "white", leg_right + position, body_bottom + position, 3)
        pair = [(Vector2(-9, 10)), (Vector2(9, 10))]
        pairs = [pair]
        for pair in pairs:
            self.draw_one_line(pair, position, screen, theta)

Extract method:

    def draw(self, screen):
        theta = self.theta
        head_off = Vector2(0,16+8).rotate(theta)
        position = self.position
        pygame.draw.circle(screen, "white", position + head_off, 8, 2)
        body_top = Vector2(0, 16).rotate(theta)
        body_bottom = Vector2(0, 2).rotate(theta)
        pygame.draw.line(screen, "white", body_top + position, body_bottom + position, 3)
        leg_left = Vector2(-5, -16).rotate(theta)
        pygame.draw.line(screen, "white", leg_left + position, body_bottom + position, 3)
        leg_right = Vector2(5, -16).rotate(theta)
        pygame.draw.line(screen, "white", leg_right + position, body_bottom + position, 3)
        pair = [(Vector2(-9, 10)), (Vector2(9, 10))]
        pairs = [pair]
        self.draw_lines(pairs, position, screen, theta)

    def draw_lines(self, pairs, position, screen, theta):
        for pair in pairs:
            self.draw_one_line(pair, position, screen, theta)

Forgot a commit, I was trying to do them on each step. Commit: draw_lines function.

Now I think that I’d like to just move all my explicit vectors into a series of pairs.

Rename variable:

        arm = [(Vector2(-9, 10)), (Vector2(9, 10))]
        pairs = [arm]
        self.draw_lines(pairs, position, screen, theta)

Create some more pairs from the existing code: First extract some variables:

    def draw(self, screen):
        theta = self.theta
        head_off = Vector2(0,16+8).rotate(theta)
        position = self.position
        pygame.draw.circle(screen, "white", position + head_off, 8, 2)
        body_top_raw = Vector2(0, 16)
        body_top = body_top_raw.rotate(theta)
        body_bottom_raw = Vector2(0, 2)
        body_bottom = body_bottom_raw.rotate(theta)
        pygame.draw.line(screen, "white", body_top + position, body_bottom + position, 3)
        leg_left_raw = Vector2(-5, -16)
        leg_left = leg_left_raw.rotate(theta)
        pygame.draw.line(screen, "white", leg_left + position, body_bottom + position, 3)
        leg_right_raw = Vector2(5, -16)
        leg_right = leg_right_raw.rotate(theta)
        pygame.draw.line(screen, "white", leg_right + position, body_bottom + position, 3)
        arm = [(Vector2(-9, 10)), (Vector2(9, 10))]
        pairs = [arm]
        self.draw_lines(pairs, position, screen, theta)

Move the loop to the top:

    def draw(self, screen):
        theta = self.theta
        head_off = Vector2(0,16+8).rotate(theta)
        position = self.position
        pygame.draw.circle(screen, "white", position + head_off, 8, 2)
        arm = [(Vector2(-9, 10)), (Vector2(9, 10))]
        pairs = [arm]
        self.draw_lines(pairs, position, screen, theta)
        body_top_raw = Vector2(0, 16)
        body_top = body_top_raw.rotate(theta)
        body_bottom_raw = Vector2(0, 2)
        body_bottom = body_bottom_raw.rotate(theta)
        pygame.draw.line(screen, "white", body_top + position, body_bottom + position, 3)
        leg_left_raw = Vector2(-5, -16)
        leg_left = leg_left_raw.rotate(theta)
        pygame.draw.line(screen, "white", leg_left + position, body_bottom + position, 3)
        leg_right_raw = Vector2(5, -16)
        leg_right = leg_right_raw.rotate(theta)
        pygame.draw.line(screen, "white", leg_right + position, body_bottom + position, 3)

Move all the raw vectors up:

    def draw(self, screen):
        theta = self.theta
        head_off = Vector2(0,16+8).rotate(theta)
        position = self.position
        pygame.draw.circle(screen, "white", position + head_off, 8, 2)
        body_top_raw = Vector2(0, 16)
        body_bottom_raw = Vector2(0, 2)
        leg_left_raw = Vector2(-5, -16)
        leg_right_raw = Vector2(5, -16)
        arm = [(Vector2(-9, 10)), (Vector2(9, 10))]
        pairs = [arm]
        self.draw_lines(pairs, position, screen, theta)
        body_top = body_top_raw.rotate(theta)
        body_bottom = body_bottom_raw.rotate(theta)
        pygame.draw.line(screen, "white", body_top + position, body_bottom + position, 3)
        leg_left = leg_left_raw.rotate(theta)
        pygame.draw.line(screen, "white", leg_left + position, body_bottom + position, 3)
        leg_right = leg_right_raw.rotate(theta)
        pygame.draw.line(screen, "white", leg_right + position, body_bottom + position, 3)

Now make the pairs:

    def draw(self, screen):
        theta = self.theta
        head_off = Vector2(0,16+8).rotate(theta)
        position = self.position
        pygame.draw.circle(screen, "white", position + head_off, 8, 2)
        body_top_raw = Vector2(0, 16)
        body_bottom_raw = Vector2(0, 2)
        body = [body_bottom_raw, body_top_raw]
        leg_left_raw = Vector2(-5, -16)
        left_leg = [leg_left_raw, body_bottom_raw]
        leg_right_raw = Vector2(5, -16)
        right_leg = [leg_right_raw, body_bottom_raw]
        arm = [(Vector2(-9, 10)), (Vector2(9, 10))]
        pairs = [arm]
        self.draw_lines(pairs, position, screen, theta)
        body_top = body_top_raw.rotate(theta)
        body_bottom = body_bottom_raw.rotate(theta)
        pygame.draw.line(screen, "white", body_top + position, body_bottom + position, 3)
        leg_left = leg_left_raw.rotate(theta)
        pygame.draw.line(screen, "white", leg_left + position, body_bottom + position, 3)
        leg_right = leg_right_raw.rotate(theta)
        pygame.draw.line(screen, "white", leg_right + position, body_bottom + position, 3)

Commit: making pairs for body.

Inline the values for the pairs.

    def draw(self, screen):
        theta = self.theta
        head_off = Vector2(0,16+8).rotate(theta)
        position = self.position
        pygame.draw.circle(screen, "white", position + head_off, 8, 2)
        body_top_raw = Vector2(0, 16)
        body_bottom_raw = Vector2(0, 2)
        body = [body_bottom_raw, body_top_raw]
        left_leg = [(Vector2(-5, -16)), body_bottom_raw]
        right_leg = [(Vector2(5, -16)), body_bottom_raw]
        arm = [(Vector2(-9, 10)), (Vector2(9, 10))]
        pairs = [arm]
        self.draw_lines(pairs, position, screen, theta)
        body_top = body_top_raw.rotate(theta)
        body_bottom = body_bottom_raw.rotate(theta)
        pygame.draw.line(screen, "white", body_top + position, body_bottom + position, 3)
        leg_left = Vector2(-5, -16).rotate(theta)
        pygame.draw.line(screen, "white", leg_left + position, body_bottom + position, 3)
        leg_right = Vector2(5, -16).rotate(theta)
        pygame.draw.line(screen, "white", leg_right + position, body_bottom + position, 3)

Commit: inline constant vectors.

Make the list and remove all the specialized draw calls:

    def draw(self, screen):
        theta = self.theta
        head_off = Vector2(0,16+8).rotate(theta)
        position = self.position
        pygame.draw.circle(screen, "white", position + head_off, 8, 2)
        body_top_raw = Vector2(0, 16)
        body_bottom_raw = Vector2(0, 2)
        body = [body_bottom_raw, body_top_raw]
        left_leg = [(Vector2(-5, -16)), body_bottom_raw]
        right_leg = [(Vector2(5, -16)), body_bottom_raw]
        arm = [(Vector2(-9, 10)), (Vector2(9, 10))]
        pairs = [body, arm, left_leg, right_leg]
        self.draw_lines(pairs, position, screen, theta)

Commit: GFragment draw draws all lines via draw_lines function.

Now I would like to inline the body top, just keeping the one that is reused, body_bottom_raw. And rename it.

    def draw(self, screen):
        theta = self.theta
        head_off = Vector2(0,16+8).rotate(theta)
        position = self.position
        pygame.draw.circle(screen, "white", position + head_off, 8, 2)
        body_bottom = Vector2(0, 2)
        body = [body_bottom, (Vector2(0, 16))]
        left_leg = [(Vector2(-5, -16)), body_bottom]
        right_leg = [(Vector2(5, -16)), body_bottom]
        arm = [(Vector2(-9, 10)), (Vector2(9, 10))]
        pairs = [body, arm, left_leg, right_leg]
        self.draw_lines(pairs, position, screen, theta)

Commit: inline and rename variables.

I’d like to change the signature of the function, Parms should be in a more common order:

    def draw(self, screen):
        theta = self.theta
        head_off = Vector2(0,16+8).rotate(theta)
        position = self.position
        pygame.draw.circle(screen, "white", position + head_off, 8, 2)
        body_bottom = Vector2(0, 2)
        body = [body_bottom, (Vector2(0, 16))]
        left_leg = [(Vector2(-5, -16)), body_bottom]
        right_leg = [(Vector2(5, -16)), body_bottom]
        arm = [(Vector2(-9, 10)), (Vector2(9, 10))]
        pairs = [body, arm, left_leg, right_leg]
        self.draw_lines(screen, position, theta, pairs)

    def draw_lines(self, screen, position, theta, pairs):
        for pair in pairs:
            self.draw_one_line(pair, position, screen, theta)

Let’s inline the theta and position, they’re not helping much. And remove extra parens that the inlining inserted:

    def draw(self, screen):
        head_off = Vector2(0, 16 + 8).rotate(self.theta)
        pygame.draw.circle(screen, "white", self.position + head_off, 8, 2)
        body_bottom = Vector2(0, 2)
        body = [Vector2(0, 16), body_bottom]
        left_leg = [Vector2(-5, -16), body_bottom]
        right_leg = [Vector2(5, -16), body_bottom]
        arm = [Vector2(-9, 10), Vector2(9, 10)]
        pairs = [body, arm, left_leg, right_leg]
        self.draw_lines(screen, self.position, self.theta, pairs)

Commit: minor cleanup.

Now then. We can fix VFragment to use this new function.

    def draw(self, screen):
        side_1 = [Vector2(-7, 5), Vector2(7, 0)]
        side_2 = [Vector2(7, 0), Vector2(-7, -5)]
        v = [side_1, side_2]
        self.draw_lines(screen, self.position, self.theta, v)

I have to move draw_lines and draw_one_line up to Fragment for that to work. So sue me for using implementation inheritance.

Commit: VFragment uses draw_lines.

Now for Fragment itself:

    def draw(self, screen):
        begin = self.position + self.begin.rotate(self.theta)
        end = self.position + self.end.rotate(self.theta)
        pygame.draw.line(screen, "white", begin, end, 3)

It is kind enough to have its pieces already defined:

    def draw(self, screen):
        frag = [self.begin, self.end]
        self.draw_one_line(screen, self.position, self.theta, frag)

Commit: Fragment uses draw_one_line.

One thing we might want to worry about is that in VFragment and GFragment, we compute the arrays of points every time. They do not vary, as they are entirely constant.

Let’s make an odd change. Let’s make Fragment use draw_lines even though it only draws one line.

    def draw(self, screen):
        frags = [[self.begin, self.end]]
        self.draw_lines(screen, self.position, self.theta, frags)

Commit: change Fragment to use *draw_lines in prep for preparing fragments in init.*

Now I plan to have the Fragment class create its frags in a new method, maybe named create_fragments, and then the others will override that method.

We’ll do this in a few steps, since I’m going for a world record number of commits (and apparently a world record article length).

class Fragment:
    def __init__(self, position, angle=None, speed_mul=None):
        angle = angle if angle is not None else random.randrange(360)
        self.position = position
        half_length = random.uniform(6, 10)
        self.begin = Vector2(-half_length, 0)
        self.end = Vector2(half_length, 0)
        speed_mul = speed_mul if speed_mul is not None else random.uniform(0.25, 0.5)
        self.velocity = speed_mul*Vector2(u.FRAGMENT_SPEED, 0).rotate(angle)
        self.theta = random.randrange(0, 360)
        self.delta_theta = random.uniform(180, 360)*random.choice((1, -1))
        self.timer = Timer(u.FRAGMENT_LIFETIME, self.timeout)

Move begin and end down to the bottom.

class Fragment:
    def __init__(self, position, angle=None, speed_mul=None):
        angle = angle if angle is not None else random.randrange(360)
        self.position = position
        speed_mul = speed_mul if speed_mul is not None else random.uniform(0.25, 0.5)
        self.velocity = speed_mul*Vector2(u.FRAGMENT_SPEED, 0).rotate(angle)
        self.theta = random.randrange(0, 360)
        self.delta_theta = random.uniform(180, 360)*random.choice((1, -1))
        self.timer = Timer(u.FRAGMENT_LIFETIME, self.timeout)
        half_length = random.uniform(6, 10)
        self.begin = Vector2(-half_length, 0)
        self.end = Vector2(half_length, 0)

Commit: move frag creation down.

Now let’s create the nested list here and use it in draw:

class Fragment:
    def __init__(self, position, angle=None, speed_mul=None):
        angle = angle if angle is not None else random.randrange(360)
        self.position = position
        speed_mul = speed_mul if speed_mul is not None else random.uniform(0.25, 0.5)
        self.velocity = speed_mul*Vector2(u.FRAGMENT_SPEED, 0).rotate(angle)
        self.theta = random.randrange(0, 360)
        self.delta_theta = random.uniform(180, 360)*random.choice((1, -1))
        self.timer = Timer(u.FRAGMENT_LIFETIME, self.timeout)
        half_length = random.uniform(6, 10)
        begin = Vector2(-half_length, 0)
        end = Vector2(half_length, 0)
        self.frags = [[begin, end]]

    def draw(self, screen):
        self.draw_lines(screen, self.position, self.theta, self.frags)

Commit: draw refers to self.frags.

Extract create_fragments:

class Fragment:
    def __init__(self, position, angle=None, speed_mul=None):
        angle = angle if angle is not None else random.randrange(360)
        self.position = position
        speed_mul = speed_mul if speed_mul is not None else random.uniform(0.25, 0.5)
        self.velocity = speed_mul*Vector2(u.FRAGMENT_SPEED, 0).rotate(angle)
        self.theta = random.randrange(0, 360)
        self.delta_theta = random.uniform(180, 360)*random.choice((1, -1))
        self.timer = Timer(u.FRAGMENT_LIFETIME, self.timeout)
        self.create_fragments()

    def create_fragments(self):
        half_length = random.uniform(6, 10)
        begin = Vector2(-half_length, 0)
        end = Vector2(half_length, 0)
        # noinspection PyAttributeOutsideInit
        self.fragments = [[begin, end]]

    def draw(self, screen):
        self.draw_lines(screen, self.position, self.theta, self.fragments)

Commit: extract create_fragments method for override.

Now let’s override create_fragments in VFragment, and then not override draw. We can change this:

    def draw(self, screen):
        side_1 = [Vector2(-7, 5), Vector2(7, 0)]
        side_2 = [Vector2(7, 0), Vector2(-7, -5)]
        v = [side_1, side_2]
        self.draw_lines(screen, self.position, self.theta, v)

To this:

    def create_fragments(self):
        side_1 = [Vector2(-7, 5), Vector2(7, 0)]
        side_2 = [Vector2(7, 0), Vector2(-7, -5)]
        # noinspection PyAttributeOutsideInit
        self.fragments  = [side_1, side_2]

Commit: VFragment overrides create_fragments and not draw.

And now we can extract fragment creation for GFragment but still override draw.

    def draw(self, screen):
        head_off = Vector2(0, 16 + 8).rotate(self.theta)
        pygame.draw.circle(screen, "white", self.position + head_off, 8, 2)
        body_bottom = Vector2(0, 2)
        body = [Vector2(0, 16), body_bottom]
        left_leg = [Vector2(-5, -16), body_bottom]
        right_leg = [Vector2(5, -16), body_bottom]
        arm = [Vector2(-9, 10), Vector2(9, 10)]
        pairs = [body, arm, left_leg, right_leg]
        self.draw_lines(screen, self.position, self.theta, pairs)
    def draw(self, screen):
        head_off = Vector2(0, 16 + 8).rotate(self.theta)
        pygame.draw.circle(screen, "white", self.position + head_off, 8, 2)
        body_bottom = Vector2(0, 2)
        body = [Vector2(0, 16), body_bottom]
        left_leg = [Vector2(-5, -16), body_bottom]
        right_leg = [Vector2(5, -16), body_bottom]
        arm = [Vector2(-9, 10), Vector2(9, 10)]
        # noinspection PyAttributeOutsideInit
        self.fragments = [body, arm, left_leg, right_leg]
        self.draw_lines(screen, self.position, self.theta, self.fragments)

Commit: save GFragment fragments in member variable.

Now the extract for create, and one for the head for neatness:

class GFragment(Fragment):
    def __init__(self, position, angle=None, speed_mul=None):
        super().__init__(position, angle, speed_mul)

    def create_fragments(self):
        body_bottom = Vector2(0, 2)
        body = [Vector2(0, 16), body_bottom]
        left_leg = [Vector2(-5, -16), body_bottom]
        right_leg = [Vector2(5, -16), body_bottom]
        arm = [Vector2(-9, 10), Vector2(9, 10)]
        # noinspection PyAttributeOutsideInit
        self.fragments = [body, arm, left_leg, right_leg]

    def draw(self, screen):
        super().draw(screen)
        self.draw_head(screen)

    def draw_head(self, screen):
        head_off = Vector2(0, 16 + 8).rotate(self.theta)
        pygame.draw.circle(screen, "white", self.position + head_off, 8, 2)

Commit: GFragment overrides create_fragments. All three classes use create_fragments so as only to compute their lists once.

And I think we’re done. We’ve committed a cardinal sin according to GeePaw Hill, inheriting implementation from Fragment down to VFragment and GFragment. Both classes inherit __init__ and create_fragments, and VFragment inherits draw as well, while GFragment overrides and uses the superclass draw.

Maybe we’ll look at converting to delegation in a future article but this is already too long by double.

Summary

Twenty commits over two hours. Not bad at all, and every one of them a step toward improvement.

The code went from ad-hoc drawing to a list-driven set of calls to draw lines, and the lists moved from being created on each call to draw to being created once at object creation time. And it was a longish series of tiny steps, each one either a machine refactoring or a very simple textual change.

I’ll put the before and after in the next article. See you soon!