Building A Lunar Lander Game In Python With Pygame

Learning Lab
My Journey Through Books, Discoveries, and Ideas

Building a lunar lander game in Python with pygame

The heart of the game resides in main.py, which orchestrates the entire process. It initializes Pygame, sets up the display window, and manages the main game_loop.


# Simplified game loop structure from main.py
def game_loop(mode: str):
    pygame.init()
    screen = pygame.display.set_mode((cfg.width, cfg.height))
    pygame.display.set_caption("Lunar Lander")
    clock = pygame.time.Clock()
    font = pygame.font.Font(None, 25)
    # ... Initialize sound, GameLogic, LanderVisuals ...

    running = True
    game_over = False
    has_started = False # Track if player has initiated movement

    while running:
        clock.tick(cfg.fps) # Control frame rate
        action = 0 # Default action: Noop

        # --- Event Handling ---
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_q: running = False
                # Start game logic updates once player moves (in 'play' mode)
                if mode == 'play' and event.key in [pygame.K_UP, pygame.K_LEFT, pygame.K_RIGHT]:
                    has_started = True

        # --- Action Determination (Manual Play) ---
        if not game_over and mode == 'play':
            keys = pygame.key.get_pressed()
            if keys[pygame.K_UP]: action = 1
            elif keys[pygame.K_LEFT]: action = 2
            elif keys[pygame.K_RIGHT]: action = 3

        # --- Game Logic Update ---
        # Only update if game has started and not over
        if has_started and not game_over:
            state, done = logic.update(action) # logic is GameLogic instance
            if done:
                game_over = True
                # ... Handle game over message ...

        # --- Rendering ---
        draw_game(screen, logic, visuals, sounds, font) # visuals is LanderVisuals instance

        # --- Game Over Handling ---
        if game_over:
            pygame.time.delay(1500) # Pause briefly
            running = False

    pygame.quit()

This loop continuously checks for player input (keyboard events), determines the appropriate action (thrusting up, left, or right), updates the game’s state through the GameLogic module, and then redraws the screen.

Rendering the scene

The draw_game function is responsible for painting the current state onto the screen. It uses information provided by GameLogic (get_render_info) and visual assets managed by LanderVisuals.

  1. Clear Screen: Fills the background.
  2. Draw Terrain & Pads: Simple rectangles represent the ground and the crucial landing pads (start and target).
  3. Draw Lander: The lander’s image, pre-loaded and scaled by LanderVisuals, is blitted (drawn) at its current (x, y) coordinates.
  4. Draw Flames: Based on the last_action and remaining fuel, the appropriate flame animation (vertical, left, or right) is drawn relative to the lander’s position. The LanderVisuals class handles loading and scaling these flame images.
  5. Play Sound: If thrusting and sounds are enabled, the engine sound effect is played.
  6. Draw HUD: Key information like remaining Fuel, current x/y position, and vx/vy velocity are rendered as text onto the screen.

# Snippet from draw_game in main.py
def draw_game(screen: pygame.Surface, logic: GameLogic, visuals: LanderVisuals,
              sounds: SimpleNamespace, font: pygame.font.Font):
    render_info = logic.get_render_info()
    lander_x, lander_y = render_info["x"], render_info["y"]
    fuel, vx, vy = render_info["fuel"], render_info["vx"], render_info["vy"]
    last_action = render_info["last_action"]

    screen.fill(c.k) # Black background

    # Draw terrain (white rect) and pads (red/green rects)
    pygame.draw.rect(screen, c.w, (0, cfg.height - gcfg.terrain_y, cfg.width, gcfg.terrain_y))
    pygame.draw.rect(screen, c.r, (gcfg.spad_x1, gcfg.pad_y1, gcfg.spad_width, gcfg.pad_height)) # Start pad
    pygame.draw.rect(screen, c.g, (gcfg.lpad_x1, gcfg.pad_y1, gcfg.lpad_width, gcfg.pad_height)) # Landing pad

    # Draw Lander image
    scaled_images = visuals.get_scaled_images()
    screen.blit(scaled_images["lander"], (lander_x, lander_y))

    # Draw Flames conditionally
    play_sound = False
    if last_action == 1 and fuel > 0: # Up
        screen.blit(scaled_images["vflames"], (lander_x + ..., lander_y + ...))
        play_sound = True
    elif last_action == 2 and fuel > 0: # Left
        screen.blit(scaled_images["rflames"], (lander_x + ..., lander_y + ...)) # Right flames for left thrust
        play_sound = True
    # ... elif for right thrust ...

    # Play sound
    if cfg.with_sounds and play_sound and not pygame.mixer.get_busy():
        sounds.engine_s.play()

    # Draw HUD text
    fuel_text = font.render(f'Fuel: {int(fuel)}', True, c.w)
    screen.blit(fuel_text, (10, 10))
    # ... render and blit position (p_text) and velocity (v_text) ...

    pygame.display.flip() # Update the full display surface

The LanderVisuals class (mod_lander.py) simply loads and scales the necessary PNG images (lander_1.png, vertical_flames.png, etc.) during initialization, making them readily available for the draw_game function.

Configuration

Many aspects of the game are controlled via mod_config.py. This includes:

  • Screen dimensions (cfg.width, cfg.height).
  • Lander appearance and flame offsets (lander_cfg).
  • Initial game state, pad locations (which can be randomized!), and landing difficulty (max speeds gcfg.max_vx, gcfg.max_vy).
  • Physics parameters like gravity (planet_cfg.g).

This separation makes it easy to tweak the game’s look, feel, and difficulty without digging deep into the core logic.

Physics Simulation

While this post focuses on the UI, it’s worth noting that mod_game_logic.py handles the simulation’s core physics. The GameLogic class tracks the lander’s position (x, y), velocity (vx, vy), and fuel. Its update method applies gravity, friction, and thrust based on player actions, calculates the new state, and checks for landing or crashing conditions according to the rules defined in mod_config.py.

This modular approach creates a playable Lunar Lander experience, separating concerns between game logic, visual presentation, and configuration.

For more insights into this topic, you can find the details here.

The code for this implementation is available on Github here.