Implementing Game Logic In C++ For Lunar Lander

Learning Lab
My Journey Through Books, Discoveries, and Ideas

Implementing game logic in C++ for lunar lander

At the core of any game simulation lies its logic engine. For our Lunar Lander project, this component is encapsulated in the GameLogicCpp class, written in C++. This class is responsible for managing the lander’s state, applying physics, handling actions, and determining the outcome of each landing attempt. It’s designed to be robust, configurable, and efficient, especially when used for training AI agents.

The GameLogicCpp class is templated, allowing it to work with either float or double precision for its calculations. This provides flexibility depending on the performance and accuracy requirements.

Key responsibilities include:

  • Tracking the lander’s state (position, velocity, fuel).
  • Applying physics rules (gravity, thrust, friction).
  • Detecting collisions and landing conditions.
  • Providing a state representation suitable for AI agents.
  • Calculating penalties and rewards for AI training.

State variables

The class maintains several public member variables that define the current state of the lander and the game environment:


// In GameLogicCpp.h
template <typename T> class GameLogicCpp
{
  public:
    T x, y, vx, vy, fuel; // Lander's kinematic state and fuel
    bool landed, crashed, landed_successfully; // Game outcome flags
    T landing_pad_center_x, landing_pad_y; // Landing pad position
    // ... other members ...
};
  • x, y: Horizontal and vertical position.
  • vx, vy: Horizontal and vertical velocity.
  • fuel: Remaining fuel.
  • landed, crashed, landed_successfully: Boolean flags indicating the end state of an episode.
  • landing_pad_center_x, landing_pad_y: Coordinates of the target landing zone.

Configuration and initialization

The game environment is highly configurable. The set_config method allows setting various parameters:


// In GameLogicCpp.h
void set_config(T cfg_w, T cfg_h, T gcfg_pad_y1, /* ... other params ... */);

This method initializes:

  • Screen dimensions (cfg_w, cfg_h).
  • Landing pad position and terrain height (gcfg_pad_y1, gcfg_terrain_y_val).
  • Safe landing speeds (gcfg_max_v_x, gcfg_max_v_y).
  • Physics constants like gravity and friction (pcfg_gravity, pcfg_fric_x, pcfg_fric_y).
  • Lander properties: dimensions and maximum fuel (lcfg_w, lcfg_h, lcfg_fuel).
  • Initial state of the lander (position, velocity, acceleration).

A helper, recalculate_derived_values(), computes values like ground_level based on the configuration.

Resetting the game state

Each game episode or training iteration requires resetting the lander to a starting state.

  • reset(): Resets the lander to its default initial position, velocity, and full fuel.
  • reset(T spad_x1_new, T lpad_x1_new): An overloaded version that allows specifying new starting and landing pad positions. This is particularly useful during AI training to expose the agent to varied scenarios.

// In GameLogicCpp.cpp
template <typename T>
void GameLogicCpp<T>::reset(T spad_x1_new, T lpad_x1_new)
{
    lpad_x1   = lpad_x1_new;
    // Update lander initial x position based on the new start pad
    initial_x = spad_x1_new + (spad_width / T(2.0)) - (lcfg_width / T(2.0));
    recalculate_derived_values(); // Update pad center, etc.
    reset(); // Call the base reset for other variables
}

The Game update cycle

The update method is the heart of the simulation, advancing the game by one time step. It takes an action (typically from the player or an AI) and returns the new game state and a flag indicating if the episode is done.


// In GameLogicCpp.h
// Returns new state and a 'done' flag
std::pair<std::vector<T>, bool> update(int action);
// Alternative: writes state to provided buffer
bool update(int action, T* pStateOutput, size_t stateOutputSize);

The update cycle involves several steps:

  1. apply_action(int action): Modifies the lander’s velocity and consumes fuel based on the chosen action (0: Noop, 1: Thrust Up, 2: Thrust Left, 3: Thrust Right).

// In GameLogicCpp.cpp
template <typename T>
void GameLogicCpp<T>::apply_action(int action)
{
    last_action = action;
    if (fuel <= T(0.0)) return; // No thrust if out of fuel

    switch (action)
    {
    case 1: vy -= T(0.3); fuel -= T(1.0); break; // Thrust Up
    case 2: vx -= T(0.05); fuel -= T(0.5); break; // Thrust Left
    case 3: vx += T(0.05); fuel -= T(0.5); break; // Thrust Right
    }
    fuel = std::max(T(0.0), fuel);
}
  1. update_physics(): Updates velocities based on acceleration (gravity is part of ay) and applies friction. Then, updates the lander’s position based on its new velocity.

// In GameLogicCpp.cpp
template <typename T>
void GameLogicCpp<T>::update_physics()
{
    vy += ay; // Gravity is included in ay
    vx *= (T(1.0) - pcfg_mu_x); // Friction
    vy *= (T(1.0) - pcfg_mu_y);
    x += vx;
    y += vy;
}
  1. check_landing_crash(): This crucial function determines if the lander has hit the ground. If so, it checks if the landing was within the pad boundaries and if the impact velocities were within safe limits. It then sets the landed, crashed, and landed_successfully flags accordingly.

// In GameLogicCpp.cpp
template <typename T>
void GameLogicCpp<T>::check_landing_crash()
{
    if (y >= ground_level) // Lander's bottom edge at or below ground
    {
        y = ground_level;
        landed = true;
        bool on_landing_pad = (x >= lpad_x1 && (x + lcfg_width) <= (lpad_x1 + lpad_width));
        bool safe_speed = (std::abs(vx) < max_safe_vx && std::abs(vy) < max_safe_vy);

        if (on_landing_pad && safe_speed) {
            landed_successfully = true;
            // ... stop movement ...
        } else {
            crashed = true;
            // ... stop movement ...
        }
    }
}
  1. is_done(): An helper that returns true if landed or crashed is true, signaling the end of the episode.

State representation for AI

For an AI agent to control the lander, it needs a numerical representation of the game state. The get_state() method provides this:


// In GameLogicCpp.cpp
template <typename T>
std::vector<T> GameLogicCpp<T>::get_state() const
{
    T dist_target_x = x - landing_pad_center_x;
    T dist_target_y = y - landing_pad_y;

    std::vector<T> state = {
        vx / max_safe_vx,           // Normalized Vx
        vy / max_safe_vy,           // Normalized Vy
        dist_target_x / cfg_width,  // Normalized distance X to pad
        dist_target_y / cfg_height, // Normalized distance Y to pad
        fuel / lcfg_max_fuel        // Normalized Fuel
    };
    // Values are then clamped to reasonable ranges, e.g., velocities to [-2, 2]
    state[0] = std::clamp(state[0], T(-2.0), T(2.0));
    // ... and so on for other state components ...
    return state;
}

The state vector includes normalized velocities, normalized distances to the landing pad, and normalized remaining fuel. Normalization and clipping help the AI process these values effectively. An overloaded version get_state(T* pOutputs, size_t outputsSize) allows writing the state directly into a pre-allocated buffer for efficiency.

Penalty and reward system

To train an AI using reinforcement learning or genetic algorithms, a reward (or penalty) system is essential. GameLogicCpp provides methods to calculate these:

  • calculate_step_penalty(int action): Called at each step. It applies small penalties for being far from the landing pad and for using fuel. This encourages the AI to be efficient and goal-oriented.
  • calculate_terminal_penalty(int steps_taken): Called at the end of an episode.
    • A large reward (negative penalty) is given for a successful landing, with a bonus for remaining fuel.
    • A significant penalty is applied for crashing, increased by impact speed.
    • Penalties are also given for running out of fuel before landing or simply for taking too many steps.

// In GameLogicCpp.cpp
template <typename T>
T GameLogicCpp<T>::calculate_terminal_penalty(int steps_taken) const
{
    T terminal_penalty = static_cast<T>(steps_taken) * T(0.1); // Time penalty

    if (landed_successfully) {
        terminal_penalty -= T(1000.0); // Big reward
        terminal_penalty -= fuel * T(2.0); // Fuel bonus
    } else if (crashed) {
        terminal_penalty += T(500.0); // Crash penalty
        // ... penalty for impact speed ...
    } else if (fuel <= T(0.0) && !landed) {
        terminal_penalty += T(250.0); // Out of fuel penalty
    }
    // ... other penalty components ...
    return terminal_penalty;
}

This penalty system guides the AI’s learning process towards achieving successful, efficient landings.

Conclusion

The GameLogicCpp class forms the basis for the Lunar Lander game. Its C++ implementation ensures performance, while its configurability and detailed state/reward mechanisms make it well-suited for developing and training AI controllers. By handling the complex physics and game rules, it allows other parts of the project (like the AI training or visualization) to focus on their specific tasks.

The code for this implementation is available on Github here.