Landing on the Moon with Large Language Models

Landing with AI


At the moment, we have a playable version of the lunar lander game. However, it would be nice to relax and let an AI play the game instead of us. In this chapter, we will prompt the model to generate an AI controller that plays the Lunar Lander game. We start by asking for a version where we can choose who is controlling the lunar lander (with a randomly thrusting AI), and then we will ask for an AI controller that can safely land the lunar lander.

Choosing controller

First, we prompt the large language model to modify the Lunar Lander game so that it can be controlled by an AI. The first version of the AI controller should randomly thrust to control the lunar lander.

The following outlines the design of a Lunar Lander game. Modify the game so that the lunar lander can be controlled with the AI controller. At the start of the game, the game should ask whether to play as a human, or whether the game should be controlled by the AI. As the first version of the AI, implement an AI that simply randomly thrusts.

###

(the code from the last part)

To modify the Lunar Lander game so that it can be controlled by an AI, we need to make several changes. Specifically, we need to add the option for the player to choose between human control and AI control at the start of the game. We'll also implement a simple AI that randomly decides whether to thrust.

...

The model output continued as follows.

Model output

Here’s the updated code:

import pygame
import random

# Class to manage the overall game state
class LunarLanderGame:
    def __init__(self):
        pygame.init()
        self.screen = pygame.display.set_mode((800, 600))
        pygame.display.set_caption("Lunar Lander")
        self.lander = LunarLander()
        self.surface = LunarSurface()
        self.ui = GameUI(self.screen, self.lander, self.surface)
        self.player_controller = PlayerController(self.lander)
        self.ai_controller = AIController(self.lander)
        self.score_manager = ScoreManager()
        self.is_running = True
        self.controller = None

    def start_game(self):
        # Ask the player whether to use human or AI control
        self.choose_controller()

        # Main game loop
        while self.is_running:
            self.handle_events()
            self.update_game_state()
            self.render()
            pygame.time.delay(30)  # Control frame rate

    def choose_controller(self):
        # Display options for the player to choose between human and AI control
        font = pygame.font.Font(None, 74)
        human_text = font.render("Press H for Human Control", True, (255, 255, 255))
        ai_text = font.render("Press A for AI Control", True, (255, 255, 255))

        while True:
            self.screen.fill((0, 0, 0))
            self.screen.blit(human_text, (100, 200))
            self.screen.blit(ai_text, (100, 300))
            pygame.display.flip()

            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    self.is_running = False
                    return
                elif event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_h:
                        self.controller = self.player_controller
                        return
                    elif event.key == pygame.K_a:
                        self.controller = self.ai_controller
                        return

    def handle_events(self):
        # Handle player input and other events
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                self.is_running = False
            if self.controller == self.player_controller:
                self.player_controller.handle_event(event)

    def update_game_state(self):
        # Update lander and check for collisions
        if self.controller == self.ai_controller:
            self.ai_controller.control_lander()

        self.lander.update()
        collision_status = self.surface.check_collision(self.lander)
        if collision_status is not None:
            self.is_running = False
            self.ui.display_game_over(collision_status)

    def render(self):
        # Render the game UI
        self.ui.render()

# Class to represent the Lunar Lander
class LunarLander:
    def __init__(self):
        self.position = [390, 0]  # Center the lander horizontally at the top
        self.velocity = [0, 0]
        self.fuel = 100
        self.thrusting = False

    def update(self):
        # Update position and velocity based on thrust and gravity
        self.apply_gravity()
        if self.thrusting and self.fuel > 0:
            self.apply_thrust()
        self.position[1] += self.velocity[1]

    def apply_gravity(self):
        # Simulate gravity affecting the lander
        self.velocity[1] += 0.1  # Simple gravity effect

    def apply_thrust(self):
        # Apply thrust to counteract gravity
        self.velocity[1] -= 0.5
        self.fuel -= 0.1

# Class to represent the lunar surface
class LunarSurface:
    def __init__(self):
        # Initialize lunar surface (e.g., terrain)
        self.height = 50  # Height of the terrain

    def check_collision(self, lander):
        # Check for collision with the lunar surface
        leg_bottom = lander.position[1] + 30  # Bottom of the legs
        if leg_bottom >= 600 - self.height:
            if lander.velocity[1] < 3:
                return "win"
            else:
                return "lose"
        return None

# Class to manage the graphical user interface
class GameUI:
    def __init__(self, screen, lander, surface):
        self.screen = screen
        self.lander = lander
        self.surface = surface
        self.font = pygame.font.Font(None, 36)

    def render(self):
        # Render the lander, surface, and status information
        self.screen.fill((0, 0, 0))  # Fill the screen with black

        # Draw the lunar lander
        self.draw_lander()

        # Draw the terrain as a green rectangle
        pygame.draw.rect(self.screen, (0, 255, 0), (0, 600 - self.surface.height, 800, self.surface.height))

        # Draw the HUD
        self.render_hud()

        pygame.display.flip()  # Update the screen

    def draw_lander(self):
        # Draw the lander body as a white square
        pygame.draw.rect(self.screen, (255, 255, 255), (self.lander.position[0], self.lander.position[1], 20, 20))

        # Draw the dome as a white circle
        pygame.draw.circle(self.screen, (255, 255, 255), (self.lander.position[0] + 10, self.lander.position[1]), 10)

        # Draw the legs as white lines
        pygame.draw.line(self.screen, (255, 255, 255), (self.lander.position[0], self.lander.position[1] + 20), (self.lander.position[0] - 10, self.lander.position[1] + 30))
        pygame.draw.line(self.screen, (255, 255, 255), (self.lander.position[0] + 20, self.lander.position[1] + 20), (self.lander.position[0] + 30, self.lander.position[1] + 30))

        # Draw the thrust flame as an upside-down triangle if thrusting
        if self.lander.thrusting:
            flame_color = (255, 0, 0)  # Red color for the flame
            flame_points = [
                (self.lander.position[0] + 10, self.lander.position[1] + 40),  # Bottom point
                (self.lander.position[0], self.lander.position[1] + 20),  # Top-left point
                (self.lander.position[0] + 20, self.lander.position[1] + 20)  # Top-right point
            ]
            pygame.draw.polygon(self.screen, flame_color, flame_points)

    def render_hud(self):
        # Render the HUD with altitude, velocity, and fuel level
        altitude = 600 - self.surface.height - (self.lander.position[1] + 30)  # Adjusted for leg length
        velocity = self.lander.velocity[1]
        fuel = self.lander.fuel

        altitude_text = self.font.render(f"Altitude: {altitude:.2f}", True, (255, 255, 255))
        velocity_text = self.font.render(f"Velocity: {velocity:.2f}", True, (255, 255, 255))
        fuel_text = self.font.render(f"Fuel: {fuel:.2f}", True, (255, 255, 255))

        self.screen.blit(altitude_text, (10, 10))
        self.screen.blit(velocity_text, (10, 40))
        self.screen.blit(fuel_text, (10, 70))

    def display_game_over(self, status):
        # Display the game over screen with win/lose message
        font = pygame.font.Font(None, 74)
        if status == "win":
            text = font.render("Landed safely", True, (0, 255, 0))
        else:
            text = font.render("Crash", True, (255, 0, 0))
        self.screen.blit(text, (200, 250))
        pygame.display.flip()
        pygame.time.delay(2000)  # Display for 2 seconds

# Class to handle player input
class PlayerController:
    def __init__(self, lander):
        self.lander = lander

    def handle_event(self, event):
        # Handle player input events to control the lander
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_UP:
                self.lander.thrusting = True
        elif event.type == pygame.KEYUP:
            if event.key == pygame.K_UP:
                self.lander.thrusting = False

# Class to provide AI control for the lander
class AIController:
    def __init__(self, lander):
        self.lander = lander

    def control_lander(self):
        # AI logic to control the lander
        if random.random() < 0.1:  # 10% chance to thrust
            self.lander.thrusting = True
        else:
            self.lander.thrusting = False

# Class to manage scoring and feedback
class ScoreManager:
    def __init__(self):
        self.score = 0

    def calculate_score(self, lander):
        # Calculate score based on landing success, fuel efficiency, and time taken
        pass

    def display_score(self):
        # Display the score to the player
        pass

# Initialize and start the game
if __name__ == "__main__":
    game = LunarLanderGame()
    game.start_game()
    pygame.quit()

Changes Made:

  1. Added a choose_controller Method:

    • Displays options for the player to choose between human and AI control.
    • Sets the self.controller to either self.player_controller or self.ai_controller based on the player’s input.
  2. Modified handle_events Method:

    • Only handles player input if the player controller is chosen.
  3. Modified update_game_state Method:

    • Calls the AI controller’s control_lander method if the AI controller is chosen.
  4. Implemented AIController Class:

    • Contains a control_lander method that randomly decides whether to thrust.

This setup allows the player to choose between human and AI control at the start of the game, and the AI will randomly decide whether to thrust during its control.

Reflection

At this point, the game allows choosing whether to play as a human or let the AI control the lunar lander. The AI controller randomly decides whether to thrust, which leads to the lunar lander crashing eventually. The initial screen of the game now looks as follows.

Fig 1. -- The Lunar Lander game allows choosing whether to play as a human or whether to allow the AI to control the game.

Fig 1. — The Lunar Lander game allows choosing whether to play as a human or whether to allow the AI to control the game.

Landing safely

The next step is to implement an AI controller that can safely land the lunar lander. While there are advanced approaches like proportional-integral-derivative controller for controlling systems, we use a simpler approach. In our case, we wish that the AI adjusts the thrust based on the velocity and the distance from the surface.

The following outlines the design of a Lunar Lander game. Modify the AI in the game so that it lands the lunar lander safely. Implement the AI so that it controls the velocity based on two thresholds that relate to the distance from the surface.

###

(the code from the last part)

To improve the AI controller so that it lands the lunar lander safely, we will modify the `AIController` class to adjust the thrust based on two distance thresholds from the surface. The AI will use these thresholds to control the lander's descent velocity more precisely.

...

The output from the model continued as follows.

Model output

Here’s the updated AIController class with the logic to control the lander based on the distance from the surface:

class AIController:
    def __init__(self, lander):
        self.lander = lander
        self.threshold1 = 200  # First threshold distance from the surface
        self.threshold2 = 100  # Second threshold distance from the surface
        self.safe_velocity = 2  # Safe descent velocity

    def control_lander(self):
        altitude = 600 - self.lander.position[1] - 30  # Calculate altitude (adjusted for leg length)

        if altitude > self.threshold1:
            # High altitude: minimal thrust to reduce speed
            if self.lander.velocity[1] > self.safe_velocity:
                self.lander.thrusting = True
            else:
                self.lander.thrusting = False
        elif altitude > self.threshold2:
            # Medium altitude: moderate thrust to control descent
            if self.lander.velocity[1] > self.safe_velocity / 2:
                self.lander.thrusting = True
            else:
                self.lander.thrusting = False
        else:
            # Low altitude: strong thrust to ensure safe landing
            if self.lander.velocity[1] > self.safe_velocity / 4:
                self.lander.thrusting = True
            else:
                self.lander.thrusting = False

Here’s the complete game code with the updated AIController:

import pygame
import random

# Class to manage the overall game state
class LunarLanderGame:
    def __init__(self):
        pygame.init()
        self.screen = pygame.display.set_mode((800, 600))
        pygame.display.set_caption("Lunar Lander")
        self.lander = LunarLander()
        self.surface = LunarSurface()
        self.ui = GameUI(self.screen, self.lander, self.surface)
        self.player_controller = PlayerController(self.lander)
        self.ai_controller = AIController(self.lander)
        self.score_manager = ScoreManager()
        self.is_running = True
        self.controller = None

    def start_game(self):
        # Ask the player whether to use human or AI control
        self.choose_controller()

        # Main game loop
        while self.is_running:
            self.handle_events()
            self.update_game_state()
            self.render()
            pygame.time.delay(30)  # Control frame rate

    def choose_controller(self):
        # Display options for the player to choose between human and AI control
        font = pygame.font.Font(None, 74)
        human_text = font.render("Press H for Human Control", True, (255, 255, 255))
        ai_text = font.render("Press A for AI Control", True, (255, 255, 255))

        while True:
            self.screen.fill((0, 0, 0))
            self.screen.blit(human_text, (100, 200))
            self.screen.blit(ai_text, (100, 300))
            pygame.display.flip()

            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    self.is_running = False
                    return
                elif event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_h:
                        self.controller = self.player_controller
                        return
                    elif event.key == pygame.K_a:
                        self.controller = self.ai_controller
                        return

    def handle_events(self):
        # Handle player input and other events
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                self.is_running = False
            if self.controller == self.player_controller:
                self.player_controller.handle_event(event)

    def update_game_state(self):
        # Update lander and check for collisions
        if self.controller == self.ai_controller:
            self.ai_controller.control_lander()

        self.lander.update()
        collision_status = self.surface.check_collision(self.lander)
        if collision_status is not None:
            self.is_running = False
            self.ui.display_game_over(collision_status)

    def render(self):
        # Render the game UI
        self.ui.render()

# Class to represent the Lunar Lander
class LunarLander:
    def __init__(self):
        self.position = [390, 0]  # Center the lander horizontally at the top
        self.velocity = [0, 0]
        self.fuel = 100
        self.thrusting = False

    def update(self):
        # Update position and velocity based on thrust and gravity
        self.apply_gravity()
        if self.thrusting and self.fuel > 0:
            self.apply_thrust()
        self.position[1] += self.velocity[1]

    def apply_gravity(self):
        # Simulate gravity affecting the lander
        self.velocity[1] += 0.1  # Simple gravity effect

    def apply_thrust(self):
        # Apply thrust to counteract gravity
        self.velocity[1] -= 0.5
        self.fuel -= 0.1

# Class to represent the lunar surface
class LunarSurface:
    def __init__(self):
        # Initialize lunar surface (e.g., terrain)
        self.height = 50  # Height of the terrain

    def check_collision(self, lander):
        # Check for collision with the lunar surface
        leg_bottom = lander.position[1] + 30  # Bottom of the legs
        if leg_bottom >= 600 - self.height:
            if lander.velocity[1] < 3:
                return "win"
            else:
                return "lose"
        return None

# Class to manage the graphical user interface
class GameUI:
    def __init__(self, screen, lander, surface):
        self.screen = screen
        self.lander = lander
        self.surface = surface
        self.font = pygame.font.Font(None, 36)

    def render(self):
        # Render the lander, surface, and status information
        self.screen.fill((0, 0, 0))  # Fill the screen with black

        # Draw the lunar lander
        self.draw_lander()

        # Draw the terrain as a green rectangle
        pygame.draw.rect(self.screen, (0, 255, 0), (0, 600 - self.surface.height, 800, self.surface.height))

        # Draw the HUD
        self.render_hud()

        pygame.display.flip()  # Update the screen

    def draw_lander(self):
        # Draw the lander body as a white square
        pygame.draw.rect(self.screen, (255, 255, 255), (self.lander.position[0], self.lander.position[1], 20, 20))

        # Draw the dome as a white circle
        pygame.draw.circle(self.screen, (255, 255, 255), (self.lander.position[0] + 10, self.lander.position[1]), 10)

        # Draw the legs as white lines
        pygame.draw.line(self.screen, (255, 255, 255), (self.lander.position[0], self.lander.position[1] + 20), (self.lander.position[0] - 10, self.lander.position[1] + 30))
        pygame.draw.line(self.screen, (255, 255, 255), (self.lander.position[0] + 20, self.lander.position[1] + 20), (self.lander.position[0] + 30, self.lander.position[1] + 30))

        # Draw the thrust flame as an upside-down triangle if thrusting
        if self.lander.thrusting:
            flame_color = (255, 0, 0)  # Red color for the flame
            flame_points = [
                (self.lander.position[0] + 10, self.lander.position[1] + 40),  # Bottom point
                (self.lander.position[0], self.lander.position[1] + 20),  # Top-left point
                (self.lander.position[0] + 20, self.lander.position[1] + 20)  # Top-right point
            ]
            pygame.draw.polygon(self.screen, flame_color, flame_points)

    def render_hud(self):
        # Render the HUD with altitude, velocity, and fuel level
        altitude = 600 - self.surface.height - (self.lander.position[1] + 30)  # Adjusted for leg length
        velocity = self.lander.velocity[1]
        fuel = self.lander.fuel

        altitude_text = self.font.render(f"Altitude: {altitude:.2f}", True, (255, 255, 255))
        velocity_text = self.font.render(f"Velocity: {velocity:.2f}", True, (255, 255, 255))
        fuel_text = self.font.render(f"Fuel: {fuel:.2f}", True, (255, 255, 255))

        self.screen.blit(altitude_text, (10, 10))
        self.screen.blit(velocity_text, (10, 40))
        self.screen.blit(fuel_text, (10, 70))

    def display_game_over(self, status):
        # Display the game over screen with win/lose message
        font = pygame.font.Font(None, 74)
        if status == "win":
            text = font.render("Landed safely", True, (0, 255, 0))
        else:
            text = font.render("Crash", True, (255, 0, 0))
        self.screen.blit(text, (200, 250))
        pygame.display.flip()
        pygame.time.delay(2000)  # Display for 2 seconds

# Class to handle player input
class PlayerController:
    def __init__(self, lander):
        self.lander = lander

    def handle_event(self, event):
        # Handle player input events to control the lander
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_UP:
                self.lander.thrusting = True
        elif event.type == pygame.KEYUP:
            if event.key == pygame.K_UP:
                self.lander.thrusting = False

# Class to provide AI control for the lander
class AIController:
    def __init__(self, lander):
        self.lander = lander
        self.threshold1 = 200  # First threshold distance from the surface
        self.threshold2 = 100  # Second threshold distance from the surface
        self.safe_velocity = 2  # Safe descent velocity

    def control_lander(self):
        altitude = 600 - self.lander.position[1] - 30  # Calculate altitude (adjusted for leg length)

        if altitude > self.threshold1:
            # High altitude: minimal thrust to reduce speed
            if self.lander.velocity[1] > self.safe_velocity:
                self.lander.thrusting = True
            else:
                self.lander.thrusting = False
        elif altitude > self.threshold2:
            # Medium altitude: moderate thrust to control descent
            if self.lander.velocity[1] > self.safe_velocity / 2:
                self.lander.thrusting = True
            else:
                self.lander.thrusting = False
        else:
            # Low altitude: strong thrust to ensure safe landing
            if self.lander.velocity[1] > self.safe_velocity / 4:
                self.lander.thrusting = True
            else:
                self.lander.thrusting = False

# Class to manage scoring and feedback
class ScoreManager:
    def __init__(self):
        self.score = 0

    def calculate_score(self, lander):
        # Calculate score based on landing success, fuel efficiency, and time taken
        pass

    def display_score(self):
        # Display the score to the player
        pass

# Initialize and start the game
if __name__ == "__main__":
    game = LunarLanderGame()
    game.start_game()
    pygame.quit()

This AI controller uses two thresholds to adjust the lander’s thrust based on its altitude, aiming to safely land the lunar lander by controlling its descent velocity.

Reflection

The Lunar Lander now also has an AI that safely lands the lunar lander. The AI controller uses altitude thresholds to decide on target velocity, which is then used to adjust thrusting. The lander can still land safely.

Rule-based AI systems

The above threshold-based AI is an example of a rule-based system. Rule-based systems that decide on what to do based on a set of rules that relate to inputs. As an example, in home automation, a rule-based system might consist of a set of rules that decide on what to do based on the time of day, the day of the week, and the temperature.

A very simple rule-based system for heating a house might look like this:

  • If the temperature is below 18 degrees, turn on the heater.
  • If the temperature is over 20 degrees, turn off the heater.

One could also introduce the time of day into the system, or even consider weekdays, where each day of the week could have different rules.