In the Summer of 1980, I played Asteroids at a gas station in rural West Texas. I stood on a stool to reach the controls and see the screen. Ever since then, I've wanted to make a video game. I've also wanted to have the time, skills, and resources to make a video game. In recent years, it's become much easier to bring your idea to life using tools like Godot or one of the GameMaker engines, even with very little coding experience. That's all well and good, but in the interests of curiosity and laziness, I want to know if I can prompt a Large Language Model to do it all for me.

My focus is to see just what I can coax out of an LLM to replicate a vintage Asteroids experience. I'm fairly confident I can get assistance with boilerplate blocks of code for most of it. From there, cobbling together the final game won't be a problem, but with only prompting input on my part, how close can I get? Can I generate entire game elements? Asteroids is a simple game. Can an LLM rebuild it all, tabula rasa?

For this experiment, I'll try to stick to just prompting and using whatever the LLM provides. Along the way, I'll take a look at the code. Invariably, inefficiencies are going to jump out at you. You'll see errors start to pop up as you move forward. You may see the LLM inexplicably change code that was working just fine. But you can't change it! That's against the rules. You have to get LLM to do it using plain language requests.

The Plan

Not all LLMs are made the same, of course. For this, I'll use ChatGPT 4o (the paid version), accessible via OpenAI's website or other integrated platforms. With any LLM, clarity and specificity in your prompts are critical to getting useful responses. The more precise your prompt, the better your output. In keeping within the boundaries of this experiment, I'll just ask ChatGPT how to proceed:

“I'd like to recreate the classic arcade game, Asteroids, in Python. I will ask for your help coding this, but can we first come up with a plan on how to best approach the experiment?”

As you can see in Figure 1, ChatGPT immediately comes back with a clear and concise approach. I had a pretty good idea of how to tackle the project and what questions to ask the LLM, but ChatGPT instantly offers a number of elements I hadn't considered.

Figure 1: ChatGPT's plan
Figure 1: ChatGPT's plan

Simple and elegant, I couldn't find any issues with its suggested plan of attack. First, ChatGPT calls the shots. It analyzes all of the elements of the classic game that I'll need to clone. There are three pillars for this project: the gameplay, the controls, and the game objects.

The LLM sums up the gameplay in a few concise sentences. The player pilots a ship that can rotate and thrust. The goal is to destroy the asteroids. When shot, they break into smaller pieces. End game criteria is met when the player's ship collides with an asteroid. It's recommended that you use arrow keys to control the ship and the space bar to shoot. Bog standard, as they say.

With a game this basic, it's easy to identify the various game objects. There's a ship that rotates, thrusts, and shoots on the player's command. The asteroids must move randomly and break up when they're hit, and the bullets used to shoot them need to be defined. If you want to get fancy, ChatGPT suggests adding additional enemies.

As it continues, the LLM lists the tools you'll need, the Python libraries of Pygame and Math. You're given a step-by-step project breakdown, followed by suggested development iterations and a basic reminder to debug regularly. One of my favorite things about these sorts of projects is shown in Figure 2. ChatGPT prompts you to jump right in.

Figure 2: ChatGPT is ready to go.
Figure 2: ChatGPT is ready to go.

The Code

After some experience with LLMs, I've learned that it's wise to monitor your language. It's like making a wish to a genie. The old parables say that the djinn will exploit any vagueness in your wording. Maybe you've encountered that in the form of a malicious DM during a D&D session. Fortunately, ChatGPT is much more forgiving. If I want to continue to specify parameters, I can. In fact, if I ask the LLM to ask me how it can do better, I might get much more accurate results.

Environment Set Up and Game Loop

Next, ChatGPT enthusiastically begins with the basics. Because I've asked for explanations, it holds my hand the entire way. “Great! Let's start by setting up the environment and creating a basic game loop in Python using the Pygame library. The game loop is the core of any game, where the game continually updates and renders the screen.”

Install your Pygame and Math libraries, if you haven't already. ChatGPT starts there and provides a bulleted list of what its code will accomplish. It tells you that you're going to first address items like the main game loop, basic event handling, and rendering. Then it's off to the races! In Figure 3, you'll see the ChatGPT interface and how it presents basic copyable code.

Figure 3: ChatGPT sets up Pygame.
Figure 3: ChatGPT sets up Pygame.

See Listing 1 for the full code that ChatGPT provided. It's as bare bones as you can imagine, but you have to appreciate the copious amounts of commenting. Next, it responds to my request to explain its logic. Here, the LLM shows potential to really shine as an instructional tool. It justifies every line of code with a basic explanation.

Listing 1: Game Loop Listing

import pygame
import sys

# Initialize Pygame
pygame.init()

# Set up the game window
screen_width = 800
screen_height = 600
screen = pygame.display.set_mode((screen_width, screen_height))
pygame.display.set_caption("Asteroids")

# Set up the clock for controlling the frame rate
clock = pygame.time.Clock()
fps = 60

# Main game loop
running = True
while running:
    # Event handling
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

    # Game logic (will be added later)

    # Clear the screen with a black color
    screen.fill((0, 0, 0))

    # Rendering (will be added later)

    # Update the display
    pygame.display.flip()

    # Cap the frame rate
    clock.tick(fps)

# Quit the game
pygame.quit()
sys.exit()
  • The while running loop keeps the game running as long as running is True.
  • It loops through all events using pygame.event.get(). If the event is pygame.QUIT, which occurs when the player closes the window, set running to False to exit the loop.

There's not much to get wrong here. It's basic, but nothing crashes when you run it. Time to move on. Per the plan provided, after the explanation of the code, ChatGPT offers to move on to the next step.

Creating the Spaceship and Controls

"Would you like to move on to creating the spaceship and implementing its controls next?"

"Yes!"

That was the entirety of my response, an enthusiastic “Yes!” ChatGPT has the wheel. Time to set up the first class and add a spaceship. As with the last section, ChatGPT says what it's going to code, provides that code, and then justifies it in detail. It begins by defining the Spaceship class, giving thought to things like speed, acceleration, size, and the like. These choices may seem arbitrary, but they're based on the part of ChatGPTs massive dataset that involves the classic video game. The class for the ship provides almost everything ChatGPT suggested. In the initial plan, the LLM identified the core features of the spaceship. The ship needs to be able to rotate, thrust, and shoot. If you look at the code or the explanation provided, you'll note that in this iteration, you don't yet have the ability to shoot the titular asteroids. At this point, I don't know if ChatGPT has forgotten that aspect, or if it'll implement the bullets later. After the class is created, ChatGPT creates the instance of the spaceship and adds the ship to the main game loop. In the downloadable file, Spaceship and Controls, you can see this iteration of the single game file. If I run the code, it works as advertised. Check out Figure 4.

Figure 4: The ship in the vast void of space.
Figure 4: The ship in the vast void of space.

The ship is an isosceles triangle. That's appropriate, but rather than use the vertex angle as the nose of the ship, it uses one of the sides of the triangle. The ship only flies sideways! I could wrestle with this and try to get ChatGPT to fix it, but it's playable. I'll live with it for this iteration.

After its explanation of the code, ChatGPT offers a choice: "Would you like to add boundaries so the spaceship wraps around the screen when it goes off the edges, or would you prefer to work on shooting mechanics next?"

I'm sorry I doubted you, ChatGPT.

Shooting Mechanics

I choose bullets! For the bullets, ChatGPT creates a new Bullet class and modifies the Spaceship class to shoot those bullets via a press of the space bar. In the downloadable file, Shooting Mechanics, you can review the provided code. It works. I can't say that it works well, but it works. Take a look at Figure 5 to see what I mean.

Figure 5: Now there are bullets.
Figure 5: Now there are bullets.

Notice anything? The bullets are coming from the side of the ship. Because ChatGPT set the side of the triangle as the front of the ship, that makes sense and I suppose I can make that work, but it's certainly disorienting. This is the kind of strange flaw you'll encounter when trying a project like this. It technically works, but it's still in the uncanny valley of code.

With the next step, ChatGPT suggests we work on what happens when you shoot an asteroid. It's flexible enough for you to jump around, however, and offers the chance to do so.

"Would you like to add collision detection between bullets and asteroids next, or is there another feature you'd like to work on?"

I stick to the path. "I would like to add boundaries, so the spaceship wraps around the screen when it goes off the edges."

Game Boundaries

  • If self.x < 0, the spaceship has gone off the left edge, so wrap it to the right edge by setting self.x = screen_width.
  • If self.x > screen_width, the spaceship has gone off the right edge, so wrap it to the left edge by setting self.x = 0.
  • Similarly, if self.y < 0, the spaceship wraps from the top to the bottom when you set self.y = screen_height.
  • If self.y > screen_height, the spaceship wraps from the bottom to the top by setting self.y = 0.

After each iteration, I run the code to test. So far so good. Nothing flashy, but no errors, either.

"Let's add asteroids," I prompt.

The Asteroids

As instructed, ChatGPT remembers to explain what's needed to do in this round of code. It defines what's required from the Asteroid class, providing a list of behaviors and features to account for.

class Asteroid:
    def __init__(self, x, y, size):
        self.x = x
        self.y = y
        self.size = size
        self.speed = random.uniform(1, 3)
        self.angle = random.uniform(0, 360)
        self.color = (200, 200, 200)

To run this version, grab the downloadable code named Adding the Asteroids. When you run it, you get what you see in Figure 6. The asteroids are spheres. The ship is a triangle. But it works. With each iteration, I feel like I'm building a house of cards. What will be too much for this language model to handle? Will the collision detection I need to add send this all tumbling down?

Figure 6: You're not actually going INTO an asteroid field!?
Figure 6: You're not actually going INTO an asteroid field!?

One thing jumped out at me about this last iteration.

import pygame
import sys
import math
import random

This is the first time it's called the Python library random to generate random numbers. Because ChatGPT has been so good at explaining all of its choices, I double check the Explanation of Logic it provides after every iteration. There's nothing in there detailing why it did this. Of course, you and I know it's for the various qualities of the asteroids themselves—speed, size, position — but because I haven't been inspecting every line of changed code, I have to wonder what other decisions it's made without telling me.

Collision Detection

"Would you like to add collision detection next so that bullets can destroy asteroids, or is there another feature you want to work on?"

"Yes. Collision detection, please."

This iteration of the project is found in the downloadable Python file named Collision Detection. Here, ChatGPT gives you the ability to blow stuff up. Now you have a game!

Note that within the adjusted Bullet class, ChatGPT provides this update:

def collides_with(self, asteroid):
    distance = math.hypot(self.x - asteroid.x, self.y - asteroid.y)
    return distance < self.radius + asteroid.size

The big spheres now break up into little spheres when they collide with the bullets, a key element in the classic game.

Player Lives

Here, ChatGPT guides you to select the next aspect of the game to add. You can interrupt if you see something going wrong or heading in an unhelpful direction, but so far, it all makes sense.

"Would you like to add any more features, such as a score system, player lives, or something else?"

Player lives seem a bit more important than the score, so I'm going to start there, move on to scoring, and then reassess to see what else I might need.

A few things need to happen to implement player lives. ChatGPT breaks it down:

  1. Modify the Spaceship class to track lives.
  2. Implement collision detection between the spaceship and the asteroids.
  3. Display the number of lives on screen.
Figure 7: Player lives were added in the upper left.
Figure 7: Player lives were added in the upper left.

The logic used is extraordinarily straightforward. ChatGPT explains that I added a Lives attribute to the Spaceship class. I added a method to check for collisions with asteroids.

If a collision is detected?

('spaceship.lives -= 1') 

If the player runs out of lives?

('running = False') 

Keeping Score

After we add the player lives, ChatGPT keeps moving forward, suggesting adding a scoring feature next.

"A scoring system would be great," is the only thing I need to tell it. It proceeds to walk us through adding the Score attribute to the Spaceship class and a function to increase the score based on the size of the asteroid.

There are a few key bits of code to point out in this portion. Please note that some of the code snippets presented throughout may have been broken to accommodate the limitations of print. If you'd like to copy and paste, please put the code into an unbroken line.

spaceship.score += asteroid.size based on asteroid size

and

# Display the score
score_text = font.render(f"Score: {spaceship.score}", True, (255, 255, 255))
screen.blit(score_text, (screen_width - 150, 10))

The code for this section is seen in the Keeping Score download. As you can see in Figure 8, it runs flawlessly.

Figure 8: The Score is in the upper right.
Figure 8: The Score is in the upper right.

Game Over

In minutes, you've built enough of a game to need a Game Over screen. The updated code it provides is in Listing 2. In Figure 9, you can see what happens when you try to run the game after its latest iteration.

Listing 2: Game Over Screen Listing

import pygame
import sys
import math
import random
# Initialize Pygame
pygame.init()
# Set up the game window
screen_width = 800
screen_height = 600
screen = pygame.display.set_mode((screen_width, screen_height))
pygame.display.setCaption("Asteroids")
# Set up the clock for controlling the frame rate
clock = pygame.time.Clock()
fps = 60
# Set up the font for displaying lives, score, and game over text
font = pygame.font.SysFont(None, 36)
game_over_font = pygame.font.SysFont(None, 72)
# Define the Bullet class
class Bullet:
    def __init__(self, x, y, angle):
        self.x = x
        self.y = y
        self.angle = angle
        self.speed = 10
        self.radius = 3
        self.color = (255, 255, 255)
    def move(self):
        radians = math.radians(self.angle)
        self.x += self.speed * math.cos(radians)
        self.y -= self.speed * math.sin(radians)
    def draw(self):
        pygame.draw.circle(screen, self.color, 
          (int(self.x), int(self.y)), self.radius)
    def is_off_screen(self):
        return self.x < 0 or self.x > screen_width or 
          self.y < 0 or self.y > screen_height
    def collides_with(self, asteroid):
        distance = math.hypot(self.x - asteroid.x, self.y - asteroid.y)
        return distance < self.radius + asteroid.size
# Define the Asteroid class
class Asteroid:
    def __init__(self, x, y, size):
        self.x = x
        self.y = y
        self.size = size
        self.speed = random.uniform(1, 3)
        self.angle = random.uniform(0, 360)
        self.color = (200, 200, 200)
    def move(self):
        radians = math.radians(self.angle)
        self.x += self.speed * math.cos(radians)
        self.y -= self.speed * math.sin(radians)
        # Wrap around the screen
        if self.x < 0:
            self.x = screen_width
        elif self.x > screen_width:
            self.x = 0
        if self.y < 0:
            self.y = screen_height
        elif self.y > screen_height:
            self.y = 0
    def draw(self):
        pygame.draw.circle(screen, self.color, 
          (int(self.x), int(self.y)), self.size)
    def split(self):
        if self.size > 20:  # Minimum size to split
            size = self.size // 2
            return [Asteroid(self.x, self.y, size), 
              Asteroid(self.x, self.y, size)]
        else:
            return []  # Return an empty list if the 
                       # asteroid is too small to split
# Define the Spaceship class
class Spaceship:
    def __init__(self):
        self.x = screen_width // 2
        self.y = screen_height // 2
        self.angle = 0
        self.speed = 0
        self.rotation_speed = 5
        self.acceleration = 0.1
        self.max_speed = 5
        self.lives = 3  # Start with 3 lives
        self.score = 0  # Start with a score of 0
        self.image = pygame.Surface((40, 40))
        pygame.draw.polygon(self.image, (255, 255, 255), 
          [(20, 0), (0, 40), (40, 40)])
        self.image.set_colorkey((0, 0, 0))
        self.bullets = []
    def rotate(self, direction):
        if direction == "left":
            self.angle += self.rotation_speed
        elif direction == "right":
            self.angle -= self.rotation_speed
    def thrust(self):
        radians = math.radians(self.angle)
        self.x += self.speed * math.cos(radians)
        self.y -= self.speed * math.sin(radians)
        self.speed = min(self.speed + 
          self.acceleration, self.max_speed)
        # Wrap around the screen
        if self.x < 0:
            self.x = screen_width
        elif self.x > screen_width:
            self.x = 0
        if self.y < 0:
            self.y = screen_height
        elif self.y > screen_height:
            self.y = 0
    def shoot(self):
        bullet = Bullet(self.x, self.y, self.angle)
        self.bullets.append(bullet)
    def draw(self):
        rotated_image = 
           pygame.transform.rotate(self.image, self.angle)
        rect = rotated_image.get_rect(center=(self.x, self.y))
        screen.blit(rotated_image, rect.topleft)
    def update(self, keys):
        if keys[pygame.K_LEFT]:
            self.rotate("left")
        if keys[pygame.K_RIGHT]:
            self.rotate("right")
        if keys[pygame.K_UP]:
            self.thrust()
    def reset_position(self):
        self.x = screen_width // 2
        self.y = screen_height // 2
        self.speed = 0
        self.angle = 0
    def collides_with(self, asteroid):
        distance = math.hypot(self.x - asteroid.x, 
          self.y - asteroid.y)
        return distance < 20 + asteroid.size  # 20 is the approximate 
                                              # size of the spaceship
def game_over_screen(spaceship):
    screen.fill((0, 0, 0))
    game_over_text = game_over_font.render("GAME OVER", True, 
      (255, 255, 255))
    score_text = font.render(f"Final Score: 
      {spaceship.score}", True, (255, 255, 255))
    restart_text = font.render("Press R to Restart 
      or Q to Quit", True, (255, 255, 255))
    screen.blit(game_over_text, (screen_width // 
      2 - game_over_text.get_width() // 
      2, screen_height // 3))
    screen.blit(score_text, (screen_width // 
      2 - score_text.get_width() // 
      2, screen_height // 2))
    screen.blit(restart_text, (screen_width // 
      2 - restart_text.get_width() // 
      2, screen_height // 2 + 50))
    pygame.display.flip()
    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_r:
                    return True  # Restart the game
                elif event.key == pygame.K_q:
                    pygame.quit()
                    sys.exit()
def reset_game():
    global spaceship, asteroids
    spaceship = Spaceship()
    asteroids = []
    for _ in range(5):  # Start with 5 asteroids
        x = random.randint(0, screen_width)
        y = random.randint(0, screen_height)
        size = random.randint(20, 50)
        asteroid = Asteroid(x, y, size)
        asteroids.append(asteroid)
# Create an instance of the spaceship
spaceship = Spaceship()
# Create a list of asteroids
asteroids = []
for _ in range(5):  # Start with 5 asteroids
    x = random.randint(0, screen_width)
    y = random.randint(0, screen_height)
    size = random.randint(20, 50)  
# Asteroids will have a size between 20 and 50
    asteroid = Asteroid(x, y, size)
    asteroids.append(asteroid)
# Main game loop
running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_SPACE:
                spaceship.shoot()
    keys = pygame.key.get_pressed()
    spaceship.update(keys)
    for bullet in spaceship.bullets[:]:
        bullet.move()
        if bullet.is_off_screen():
            spaceship.bullets.remove(bullet)
        else:
            for asteroid in asteroids[:]:
                if bullet.collides_with(asteroid):
                    spaceship.bullets.remove(bullet)
                    new_asteroids = asteroid.split()
                    spaceship.score += asteroid.size
                    asteroids.remove(asteroid)
                    asteroids.extend(new_asteroids)
                    break
    for asteroid in asteroids:
        asteroid.move()
        if spaceship.collides_with(asteroid):
            spaceship.lives -= 1
            if spaceship.lives <= 0:
                if game_over_screen(spaceship):
                    reset_game()
                else:
                    running = False
            else:
                spaceship.reset_position()
    screen.fill((0, 0, 0))
    spaceship.draw()
    for bullet in spaceship.bullets:
        bullet.draw()
    for asteroid in asteroids:
        asteroid.draw()
    lives_text = font.render(f"Lives: {spaceship.lives}", 
      True, (255, 255, 255))
    screen.blit(lives_text, (10, 10))
    score_text = font.render(f"Score: {spaceship.score}", 
      True, (255, 255, 255))
    screen.blit(score_text, (screen_width - 150, 10))
    pygame.display.flip()
    clock.tick(fps)
pygame.quit()
sys.exit()
Figure 9: The first error
Figure 9: The first error

You have your first failure to launch. Visual Studio immediately shows you the problem, which is an easy fix. You may ask, “Why that error?” As the project grows, so do the chances for errors, of course, but that code was settled. It was working. You didn't need to change a thing! Here's where you start to see the cracks form. Rather than go in and fix it, let's stay true to the mission and ask ChatGPT to do it.

Errors and Debugging

Here's what I provide to ChatGPT:

"We have an error.

AttributeError: module 'pygame.display' has no attribute 'setCaption'. 
Did you mean: 'set_caption'?

Rather than regurgitate the entire file, ChatGPT sees what it did wrong and provides the exact fix, as seen in Figure 10.

Figure 10: Debugging with ChatGPT
Figure 10: Debugging with ChatGPT

After that easy fix, the game launches without issue. With lives, a score, and a Game Over screen, it's starting to feel like a real game, as you can see in Figure 11. Sure, it's nothing pretty, and the Asteroids code is dirt simple, but the original file size was only 8KB.

Figure 11: Game Over.
Figure 11: Game Over.

After this, you get a mostly functional game, but it's a little wonky. I notice that sometimes the game ends after the first death. I ask ChatGPT about it.

"Now I get to the game over screen after the first death, rather than after three lives. How can I fix that?"

It immediately suggests a revised code block, as seen below:

for asteroid in asteroids:
    asteroid.move()
    if spaceship.collides_with(asteroid):
        spaceship.lives -= 1
        if spaceship.lives > 0:
            spaceship.reset_position()  # Reset the spaceship position 
                                        # if there are still lives left
        else:
            if game_over_screen(spaceship):  # Only call the game over 
                                             # screen when lives are <= 0
                reset_game()  # Restart the game
            else:
                running = False  # End the game if the player chooses to quit

After this, I notice something. I respond to ChatGPT in plain language, "Now the asteroids don't seem to collide with the ship."

ChatGPT comes back with why it thinks the error is occurring and immediately offers a fix. Here's the entire exchange:

If the asteroids are not colliding with the ship as expected, the issue might be related to the collision detection logic between the ship and the asteroids. Let's review and refine the collision detection method.

Collision Detection Review

The collides_with method in the Spaceship class checks if the distance between the spaceship and an asteroid is less than the sum of their radii. If the collision detection isn't working correctly, it could be due to the size of the spaceship being incorrectly accounted for.

Steps to Fix the Collision Detection

  1. Ensure Proper Size Calculation: Make sure the size of the spaceship is correctly factored into the collision detection. The collision should occur when the distance between the center of the spaceship and the asteroid is less than the sum of half the spaceship's size and the asteroid's size.
  2. Adjust the Collision Detection Method: Let's update the collides_with method in the Spaceship class to use the correct size and check for collisions more accurately.

Updated collides_with Method

Here's an updated version of the collides_with method for the Spaceship class:

def collides_with(self, asteroid):
    # Calculate the distance between the spaceship and the asteroid
    distance = math.hypot(self.x - asteroid.x, self.y - asteroid.y)

    # Consider the spaceship size as half of its width
    spaceship_size = 20  # Approximate size of the spaceship

    # Check if the distance is less than the sum of their radii
    return distance < spaceship_size + asteroid.size

Explanation of Changes

  • Spaceship Size: The spaceship size is set to 20, representing half the width of the triangle-shaped spaceship. You can adjust this value if the spaceship's actual size differs.
  • Collision Logic: The collision occurs if the distance between the spaceship and the asteroid is less than the sum of the spaceship's radius (approximately half its size) and the asteroid's radius (asteroid.size).

Here's how this updated collision detection method integrates into the game loop:

for asteroid in asteroids:
    asteroid.move()
    if spaceship.collides_with(asteroid):
        spaceship.lives -= 1
        if spaceship.lives > 0:
            spaceship.reset_position()  # Reset the spaceship position 
                                        # if there are still lives left
        else:
            if game_over_screen(spaceship):  # Only call the game over 
                                             # screen when lives are <= 0
                reset_game()  # Restart the game
            else:
                running = False  # End the game if the player chooses to quit

"After implementing this fix, run the game again. The asteroids should now properly collide with the spaceship, reducing lives and eventually leading to the Game Over screen when all lives are lost. Let me know if this resolves the issue or if there's anything else you'd like to adjust!"

Unfortunately, the problems don't stop there. Now the game goes straight to the Game Over screen. Each fix breaks something else. You know the drill. Each time I come back to it with an error, ChatGPT provides not only why it might not be working, but how to fix it. Throughout the process, the LLM even starts to implement print statements to help debug.

def collides_with(self, asteroid):
    distance = math.hypot(self.x - asteroid.x, self.y - asteroid.y)
    spaceship_size = 20  # Approximate size of the spaceship
    is_collision = distance < spaceship_size + asteroid.size

    # Debug print to check collision detection
    print(f"Checking collision: Distance = {distance}, 
      Threshold = {spaceship_size + asteroid.size}, Collision = {is_collision}")

    return is_collision

As an insecure coder, I often start to believe that I'm the problem. This is one of those times. I was getting into the weeds. I take a step back and request, "Please rebuild the complete file with all of these changes."

It responds immediately with the code seen in Listing 3.

Listing 3: Full Game Listing

import pygame
import sys
import math
import random
# Initialize Pygame
pygame.init()
# Set up the game window
screen_width = 800
screen_height = 600
screen = pygame.display.set_mode((screen_width, screen_height))
pygame.display.set_caption("Asteroids")
# Set up the clock for controlling the frame rate
clock = pygame.time.Clock()
fps = 60
# Set up the font for displaying lives, score, and game over text
font = pygame.font.SysFont(None, 36)
game_over_font = pygame.font.SysFont(None, 72)
# Define the Bullet class
class Bullet:
    def __init__(self, x, y, angle):
        self.x = x
        self.y = y
        self.angle = angle
        self.speed = 10
        self.radius = 3
        self.color = (255, 255, 255)
    def move(self):
        radians = math.radians(self.angle)
        self.x += self.speed * math.cos(radians)
        self.y -= self.speed * math.sin(radians)
    def draw(self): 
        pygame.draw.circle(screen, self.color, 
          (int(self.x), int(self.y)), self.radius)
    def is_off_screen(self):
        return self.x < 0 or self.x > screen_width or 
          self.y < 0 or self.y > screen_height
    def collides_with(self, asteroid):
        distance = math.hypot(self.x - asteroid.x, 
          self.y - asteroid.y)
        return distance < self.radius + asteroid.size
# Define the Asteroid class
class Asteroid:
    def __init__(self, x, y, size):
        self.x = x
        self.y = y
        self.size = size
        self.speed = random.uniform(1, 3)
        self.angle = random.uniform(0, 360)
        self.color = (200, 200, 200)
    def move(self):
        radians = math.radians(self.angle)
        self.x += self.speed * math.cos(radians)
        self.y -= self.speed * math.sin(radians)
        # Wrap around the screen
        if self.x < 0:
            self.x = screen_width
        elif self.x > screen_width:
            self.x = 0
        if self.y < 0:
            self.y = screen_height
        elif self.y > screen_height:
            self.y = 0
    def draw(self):
        pygame.draw.circle(screen, self.color, 
          (int(self.x), int(self.y)), self.size)
    def split(self):
        if self.size > 20:  # Minimum size to split
            size = self.size // 2
            return [Asteroid(self.x, self.y, size), 
              Asteroid(self.x, self.y, size)]
        else:
            return []  # Return an empty list if 
                       # the asteroid is too small to split
# Define the Spaceship class
class Spaceship:
    def __init__(self):
        self.x = screen_width // 2
        self.y = screen_height // 2
        self.angle = 0
        self.speed = 0
        self.rotation_speed = 5
        self.acceleration = 0.1
        self.max_speed = 5
        self.lives = 3  # Start with 3 lives
        self.score = 0  # Start with a score of 0
        self.image = pygame.Surface((40, 40))
        pygame.draw.polygon(self.image, 
          (255, 255, 255), [(20, 0), (0, 40), (40, 40)])
        self.image.set_colorkey((0, 0, 0))
        self.bullets = []
    def rotate(self, direction):
        if direction == "left":
            self.angle += self.rotation_speed
        elif direction == "right":
            self.angle -= self.rotation_speed
    def thrust(self):
        radians = math.radians(self.angle)
        self.x += self.speed * math.cos(radians)
        self.y -= self.speed * math.sin(radians)
        self.speed = min(self.speed + 
            self.acceleration, self.max_speed)
        # Wrap around the screen
        if self.x < 0:
            self.x = screen_width
        elif self.x > screen_width:
            self.x = 0
        if self.y < 0:
            self.y = screen_height
        elif self.y > screen_height:
            self.y = 0
    def shoot(self):
        bullet = Bullet(self.x, self.y, self.angle)
        print(f"Shooting bullet from {self.x}, {self.y}")  
# Debug bullet creation
        self.bullets.append(bullet)
    def draw(self):
        rotated_image = pygame.transform.rotate(self.image, self.angle)
        rect = rotated_image.get_rect(center=(self.x, self.y))
        screen.blit(rotated_image, rect.topleft)
    def update(self, keys):
        if keys[pygame.K_LEFT]:
            self.rotate("left")
        if keys[pygame.K_RIGHT]:
            self.rotate("right")
        if keys[pygame.K_UP]:
            self.thrust()
    def reset_position(self):
        self.x = screen_width // 2
        self.y = screen_height // 2
        self.speed = 0
        self.angle = 0
    def collides_with(self, asteroid):
        distance = math.hypot(self.x - asteroid.x, self.y - asteroid.y)
        spaceship_size = 20  # Approximate size of the spaceship
        is_collision = distance < spaceship_size + asteroid.size
        print(f"Checking collision: Distance = {distance}, 
             Threshold = {spaceship_size + asteroid.size}, 
               Collision = {is_collision}")
        return is_collision
def game_over_screen(spaceship):
    screen.fill((0, 0, 0))
    game_over_text = game_over_font.render("GAME OVER", True, 
      (255, 255, 255))
    score_text = font.render(f"Final Score: {spaceship.score}", 
      True, (255, 255, 255))
    restart_text = font.render("Press R to Restart or Q to Quit", 
        True, (255, 255, 255))
    screen.blit(game_over_text, (screen_width 
        // 2 - game_over_text.get_width() // 2, screen_height // 3))
    screen.blit(score_text, (screen_width // 2 - score_text.get_width() 
        // 2, screen_height // 2))
    screen.blit(restart_text, (screen_width // 2 - restart_text.get_width() 
        // 2, screen_height // 2 + 50))
    pygame.display.flip()
    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_r:
                    return True  # Restart the game
                elif event.key == pygame.K_q:
                    pygame.quit()
                    sys.exit()
def reset_game():
    global spaceship, asteroids
    spaceship = Spaceship()
    asteroids = []
    for _ in range(5):  # Start with 5 asteroids
        while True:
            x = random.randint(0, screen_width)
            y = random.randint(0, screen_height)
            size = random.randint(20, 50)
            if math.hypot(x - spaceship.x, y - spaceship.y) > 
                100:  # Ensure they don't spawn too close
                asteroid = Asteroid(x, y, size)
                asteroids.append(asteroid)
                break
# Create an instance of the spaceship
spaceship = Spaceship()
# Create a list of asteroids
asteroids = []
for _ in range(5):  # Start with 5 asteroids
    while True:
        x = random.randint(0, screen_width)
        y = random.randint(0, screen_height)
        size = random.randint(20, 50)  # Asteroids will have a
                                       # size between 20 and 50
        if math.hypot(x - spaceship.x, y - spaceship.y) > 
            100:  # Ensure they don't spawn too close
            asteroid = Asteroid(x, y, size)
            asteroids.append(asteroid)
            break
# Main game loop
running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_SPACE:
                spaceship.shoot()
    keys = pygame.key.get_pressed()
    spaceship.update(keys)
    for bullet in spaceship.bullets[:]:
        bullet.move()
        print(f"Bullet position: {bullet.x}, {bullet.y}")  
# Debug bullet position
        if bullet.is_off_screen():
            print("Removing bullet (off-screen)") 
 # Debug bullet removal
            spaceship.bullets.remove(bullet)
        else:
            for asteroid in asteroids[:]:
                if bullet.collides_with(asteroid):
                    print("Bullet hit asteroid!")  
# Debug bullet-asteroid collision
                    spaceship.bullets.remove(bullet)
                    new_asteroids = asteroid.split()
                    spaceship.score += asteroid.size  
# Increase score based on asteroid size
                    asteroids.remove(asteroid)
                    asteroids.extend(new_asteroids)
                    break
    for asteroid in asteroids:
        asteroid.move()
        if spaceship.collides_with(asteroid):
            spaceship.lives -= 1
            print(f"Lives remaining: {spaceship.lives}")
            if spaceship.lives > 0:
                spaceship.reset_position()
            else:
                print("Triggering Game Over Screen")
                if game_over_screen(spaceship):
                    reset_game()
                else:
                    running = False
    screen.fill((0, 0, 0))
    spaceship.draw()
    for bullet in spaceship.bullets:
        bullet.draw()
    for asteroid in asteroids:
        asteroid.draw()
    lives_text = font.render(f"Lives: {spaceship.lives}", 
        True, (255, 255, 255))
    screen.blit(lives_text, (10, 10))
    score_text = font.render(f"Score: {spaceship.score}", 
        True, (255, 255, 255))
    screen.blit(score_text, (screen_width - 150, 10))
    pygame.display.flip()
    clock.tick(fps)
pygame.quit()
sys.exit()

The Game

It works and it's actually kind of fun! Is this the best way to build an Asteroids clone? Absolutely not! But it's certainly a good litmus test to see how LLMs can be used going forward. As with all AI, you have to remember this mantra: This is as bad as it will ever get. The technology is advancing at a rate that's hard to conceive. As I was working on this article, Google released an AI generated version of Doom. It doesn't take a lot of imagination to see where this is heading.

There's one more thing I want to try. How easy would it be to replace those asteroids with images?

"Are we able to exchange the asteroids for bitmaps or some other image?" I ask in Figure 12.

Figure 12: Enhancements
Figure 12: Enhancements

This really kicks the game up to the next level. I can do away with the crude shapes and import my own pictures. I chose CODE Magazine's own Editor-In-Chief, Mr. Rod Paddock. He makes a great celestial hazard in Figure 13, don't you think?

Figure 13: Importing images
Figure 13: Importing images

In the end, with little human interference, I get a fun and recognizable rebuild of Asteroids. It's certainly not the best way to approach the task, but if used correctly, LLMs are a powerful coding partner. The link to the unedited chat with ChatGPT is provided in the sidebar. Although its limitations are on full display in this experiment, it also promises that a truly staggering change is coming, not just to game design, but coding in general.