Configuring The C++ Lunar Lander

Learning Lab
My Journey Through Books, Discoveries, and Ideas

Configuring the C++ lunar lander

The Lunar Lander simulation has many parameters that might need adjustment:

  • Game Physics: Gravity, friction, lander fuel, safe landing speeds.
  • Game Setup: Screen dimensions, initial lander state, landing pad positions.
  • AI Training: Neural network architecture, population size, learning epochs, save intervals.
  • Application Settings: Verbosity, sound enablement, image saving paths.

Hardcoding these values would be impractical. A configuration file provides a central place to manage them.

Structure: Namespaces and Global Variables

The configuration values are organized into several namespaces within the Config namespace in config_loader.h. Each namespace groups related parameters:


// In config_loader.h
namespace Config
{
    namespace LanderCfg { /* Lander physical properties */ }
    namespace Cfg { /* General application settings */ }
    namespace GameCfg { /* Game simulation rules and initial states */ }
    namespace PlanetCfg { /* Physics environment (gravity, friction) */ }
    namespace NNConfig { /* Neural network and training settings */ }

    // Function to load configuration
    bool loadConfiguration(const std::string& filepath);
}

Inside each namespace, extern variables are declared. For example:


// In config_loader.h (within Config::NNConfig)
namespace NNConfig
{
    extern std::string name;
    extern std::vector<int> hlayers;
    extern bool use_float;
    extern int seed;
    // ... many more NN settings
}

The actual storage for these variables is defined in config_loader.cpp. This design makes the configuration values globally accessible throughout the C++ parts of the application after they’ve been loaded.

The Configuration File Format

The system expects a text file (e.g., config.txt) with key-value pairs:


# This is a comment
Cfg.width = 800.0
Cfg.height = 600.0
Cfg.verbose = true

NNConfig.name = lunar_lander_ai
NNConfig.hlayers = 64,32 # Comma-separated for vectors
NNConfig.epochs = 1000
  • Lines starting with # are comments and are ignored.
  • Empty lines are ignored.
  • Each setting is key = value.
  • Keys typically include the namespace, e.g., Cfg.width or NNConfig.hlayers.

Loading and Parsing

The core logic resides in the Config::loadConfiguration(const std::string& filepath) function in config_loader.cpp.

  1. Reading the File:

    • It opens the specified file.
    • Reads line by line.
    • Trims whitespace from each line.
    • Skips comments and empty lines.
    • Splits each valid line at the = delimiter into a key and a value string.
    • Stores these raw key-value string pairs in an std::map<std::string, std::string> rawConfig.

// In config_loader.cpp
std::ifstream configFile(filepath, std::ios::binary);
// ... error check ...
std::map<std::string, std::string> rawConfig;
while (std::getline(configFile, line))
{
    // ... trim, skip comments/empty ...
    size_t delimiterPos = line.find('=');
    // ... error check ...
    std::string key   = trim(line.substr(0, delimiterPos));
    std::string value = trim(line.substr(delimiterPos + 1));
    rawConfig[key]    = value;
}
  1. Parsing Values:
    • After reading all lines, the function iterates through the expected configuration keys.
    • For each key, it retrieves the string value from rawConfig.at(key).
    • It then parses this string value into the appropriate C++ type (e.g., double, int, bool, std::vector<int>).
    • Helper lambdas like get_bool, get_vec_double, and get_vec_int are used for type conversion and parsing comma-separated lists.

// In config_loader.cpp, inside loadConfiguration
try
{
    // Helper to parse boolean "true" or "false"
    auto get_bool = [&] (const std::string& val_str) { /* ... */ };
    // Helper to parse "1,2,3" into std::vector<int>
    auto get_vec_int = [&] (const std::string& val_str) { /* ... */ };

    // Example: Parsing Cfg settings
    Cfg::width = std::stod(rawConfig.at("Cfg.width"));
    Cfg::verbose = get_bool(rawConfig.at("Cfg.verbose"));

    // Example: Parsing NNConfig settings
    NNConfig::name = rawConfig.at("NNConfig.name");
    NNConfig::hlayers = get_vec_int(rawConfig.at("NNConfig.hlayers"));
    NNConfig::epochs = std::stoi(rawConfig.at("NNConfig.epochs"));

    // ... and so on for all other configuration variables ...
}
catch (const std::out_of_range& oor) { /* Error: key not found */ }
catch (const std::invalid_argument& ia) { /* Error: bad value format */ }
// ...
  • std::stod, std::stoi are used for string-to-double and string-to-integer conversions.
  • The split helper function (defined at the top of config_loader.cpp) is used by get_vec_double and get_vec_int to handle comma-separated values.
  • Robust error handling using try-catch blocks deals with missing keys or malformed values.

Usage in the application

Config::loadConfiguration("config.txt"); is called once at the beginning of the program’s execution (e.g., in main() of main_train.cpp).


// In main_train.cpp
#include "config_loader.h"

int main(int argc, char* argv[])
{
    if (!Config::loadConfiguration("config.txt"))
    {
        std::cerr << "FATAL: Could not load configuration. Exiting." << std::endl;
        return 1;
    }
    // Now Config::Cfg::width, Config::NNConfig::epochs, etc. are available
    // ... rest of the program ...
}

Once loaded, other parts of the C++ code can directly access the configuration values, for example:


// Elsewhere in the code
if (Config::Cfg::verbose) {
    std::cout << "Verbose mode enabled." << std::endl;
}
int num_epochs = Config::NNConfig::epochs;

Advantages

  • Simplicity: The file format is easy for humans to read and edit.
  • Flexibility: Parameters can be changed without recompiling.
  • Centralization: All core settings are in one place.
  • Type Safety (at load time): The loader attempts to parse values into their correct C++ types, catching many format errors early.

This config has been now generalized and is part of ma-libs here as libconfig_loader.

The code for this implementation is available on Github here.