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.widthorNNConfig.hlayers.
Loading and Parsing
The core logic resides in the Config::loadConfiguration(const std::string& filepath) function in config_loader.cpp.
-
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;
}
- 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, andget_vec_intare 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::stoiare used for string-to-double and string-to-integer conversions.- The
splithelper function (defined at the top ofconfig_loader.cpp) is used byget_vec_doubleandget_vec_intto handle comma-separated values. - Robust error handling using
try-catchblocks 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.