I noticed a graphical anomaly and think I can fix it. And another that confuses me.

I noticed that when an asteroid has horizontal lines at the bottom, or vertical on the right side, the lines seem more narrow:

asteroid with narrow lines

Note the bottom and right lines on the asteroid near 3 o’clock. They appear narrower than the others, including other lines at top and left when they appear. I have a guess at what is happening. We are drawing the asteroid lines at width 3, and because the size of the Surface is just enough to hold the points, part of the line is drawn outside the surface and ignored. We need to make the surface just a bit larger to handle this case.

And in looking to see where to do that, I find another issue:

    @staticmethod
    def asteroid_surface(size):
        shape = random.randint(0, 3)
        scale = [4, 8, 16][clamp(size, 0, 3)]
        return SurfaceMaker.create_scaled_surface((128, 128), Vector2(4, 4), scale, raw_rocks[shape])

The number 128 is what needs to be changed … and why are we making a huge surface for a small asteroid? It’s probably mostly harmless, although I wonder about where the center will be found on the smaller ones. How do we draw them, anyway?

    def draw(self, screen):
        half = vector2(self.surface.get_size()) / 2
        screen.blit(self.surface, self.position - half)

Ah. So small asteroids will not be centered where I think they should be. This needs improvement. And, if the surface has to be slightly larger than you’d think, we can’t use the screen size at all. We’ll need to save the official offset, I think.

Let’s review all of Asteroid.

class Asteroid:
    def __init__(self):
        height = random.randrange(0, u.SCREEN_SIZE)
        self.position = vector2(0, height)
        angle_of_travel = random.randint(0, 360)
        self.velocity = u.ASTEROID_SPEED.rotate(angle_of_travel)
        self.surface = SurfaceMaker.asteroid_surface(size=2)

    def move(self, dt):
        self.position += self.velocity*dt
        self.position.x = self.position.x % u.SCREEN_SIZE
        self.position.y = self.position.y % u.SCREEN_SIZE

    def draw(self, screen):
        half = vector2(self.surface.get_size()) / 2
        screen.blit(self.surface, self.position - half)

We know that we want to specify the size, and possibly the position. We can default them both for now.

This does not work:

    def __init__(self, size=2, position=Vector2(0, random.randrange(0, u.SCREEN_SIZE))):
        self.position = position
        offset_amount = [16, 32, 64][clamp(size, 0, 3)]
        self.offset = Vector2(offset_amount, offset_amount)
        angle_of_travel = random.randint(0, 360)
        self.velocity = u.ASTEROID_SPEED.rotate(angle_of_travel)
        self.surface = SurfaceMaker.asteroid_surface(size=2)

The reason is that the default for a parameter is evaluated only once, when the class is created, not when instances are created. We’ll try this:

    def __init__(self, size=2, position=None):
        if not position:
            self.position = Vector2(0, random.randrange(0, u.SCREEN_SIZE))
        else:
            self.position = position
        offset_amount = [16, 32, 64][clamp(size, 0, 3)]
        self.offset = Vector2(offset_amount, offset_amount)
        angle_of_travel = random.randint(0, 360)
        self.velocity = u.ASTEROID_SPEED.rotate(angle_of_travel)
        self.surface = SurfaceMaker.asteroid_surface(size=2)

I think that’s legit. Can we do this instead?

    def __init__(self, size=2, position=None):
        self.position = position if position else Vector2(0, random.randrange(0, u.SCREEN_SIZE))
        offset_amount = [16, 32, 64][clamp(size, 0, 3)]
        self.offset = Vector2(offset_amount, offset_amount)
        angle_of_travel = random.randint(0, 360)
        self.velocity = u.ASTEROID_SPEED.rotate(angle_of_travel)
        self.surface = SurfaceMaker.asteroid_surface(size)

It appears that we can. I prefer that. Hearkens back to the ternary operator. One more change, use the offset in the drawing. And I think I’ll draw a little dot at the supposed center. Let’s do that first.

small asteroids off center

As you can see, if I make them small, they are way off the center of the surface. Let’s make the surface right-sized first:

    @staticmethod
    def asteroid_surface(size):
        shape = random.randint(0, 3)
        scale = [4, 8, 16][clamp(size, 0, 3)]
        surface_size = [32, 64, 128][clamp(size, 0, 3)]
        surface = SurfaceMaker.create_scaled_surface((surface_size, surface_size), Vector2(4, 4), scale, raw_rocks[shape])
        x, y = surface.get_size()
        pygame.draw.circle(surface, "red", (x/2, y/2),  3)
        return surface

centered dots

But they’ve still got those thin lines. We have to make the surfaces one or two pixels larger.

    def asteroid_surface(size):
        shape = random.randint(0, 3)
        scale = [4, 8, 16][clamp(size, 0, 3)]
        room_for_fat_line = 1
        surface_size = [32, 64, 128][clamp(size, 0, 3)] + room_for_fat_line
        surface = SurfaceMaker.create_scaled_surface((surface_size, surface_size), Vector2(4, 4), scale, raw_rocks[shape])
        x, y = surface.get_size()
        # pygame.draw.circle(surface, "red", (x/2, y/2),  3)
        return surface

I used the pattern Explaining Constant Name to show why we’re adding one to the size. That seemed better to me than having 33, 65, and 129 in there.

Now one more thing, using the offset in draw.

    def draw(self, screen):
        screen.blit(self.surface, self.position - self.offset)

We should be good to go. Let’s put down a dot here to be sure. I think this is correct now:

class Asteroid:
    def __init__(self, size=2, position=None):
        self.position = position if position else Vector2(0, random.randrange(0, u.SCREEN_SIZE))
        offset_amount = [16, 32, 64][clamp(size, 0, 2)]
        self.offset = Vector2(offset_amount, offset_amount)
        print(self.offset)
        angle_of_travel = random.randint(0, 360)
        self.velocity = u.ASTEROID_SPEED.rotate(angle_of_travel)
        self.surface = SurfaceMaker.asteroid_surface(size)

    def move(self, dt):
        self.position += self.velocity * dt
        self.position.x = self.position.x % u.SCREEN_SIZE
        self.position.y = self.position.y % u.SCREEN_SIZE

    def draw(self, screen):
        top_left_corner = self.position - self.offset
        pygame.draw.circle(screen, "red", self.position, 3)
        screen.blit(self.surface, top_left_corner)

The picture looks right, and I’m sure the dot must be at self.position:

asteroids with dots in center

I think I tried all combinations of plus and minus to get that right. The fact that Y goes downward on the screen boggles me, and while I could draw a little picture, I always forget to do that.

Let’s commit this: Adjust surface sizes, get correct offset for asteroids.

There is another display issue that I mentioned. My monitor is allegedly 3480x2160, but when I ask PyGame to draw a 1024x1024 screen, it seems to fill almost the entire height of the monitor.

full screen shows huge asteroids window

I’m not sure what’s up with that. Could it be that my monitor is being perceived by the Air as half-sized? Or is it something PyGame is doing? I’ll have to look into it. MacOS reports that the picture is 3480x2160, so it must be something PyGame is doing. I shall inquire of my colleagues.

For now, it looks as I expect except large, so we’ll proceed.

See you next time!