Using Value Lists
When configuring the position and size of rooms, specifying each value individually for every rectangle can quickly become repetitive and cluttered. To make your configuration files cleaner and easier to maintain, it’s often more practical to use grouped value lists.
This tutorial introduces three alternative keys you can use to define rectangles more concisely: rectangle
, position
, and size
. These can be used interchangeably in the configuration file, giving you the flexibility to choose whichever makes the structure clearer and easier to read.
Below is an example that demonstrates how you can use these alternatives in practice:
1*[field.room]
2rectangle: 2, 2, 16, 3
3
4*[field.room]
5position: 2, 2
6size: 6, 12
7
8*[field.room]
9x: 2
10y: 11
11width: 16
12height: 3
13
14*[field.room]
15rectangle: 12, 2, 6, 12
16*[field.room]
17rectangle: 0, 4, 2, 2
18*[field.room]
19rectangle: 18, 8, 5, 1
20*[field.room]
21rectangle: 18, 12, 3, 3
Improving our Configuration Logic
To support these flexible input options in code, we need to enhance how we read the rectangle values. We’ll introduce a new method called rectFromSection()
that can interpret the configuration from any of the three input styles.
In addition, we’ll add exitWithErrorInValue()
to handle errors gracefully whenever a required value is missing or misconfigured.
45 [[noreturn]] static void exitWithErrorInValue(const ValuePtr &value, const std::string &message) {
46 std::cerr << message
47 << " For value '"
48 << value->namePath().toText().toCharString()
49 << "' at "
50 << value->location().toText().toCharString();
51 exit(1);
52 }
53
54 [[nodiscard]] static auto rectFromSection(const ValuePtr &value) -> Rectangle {
55 Rectangle result;
56 if (value->hasValue(u8"rectangle")) {
57 const auto rectList = value->getListOrThrow<int>(u8"rectangle");
58 if (rectList.size() != 4) { exitWithErrorInValue(value, "Rectangle must have exactly four elements."); }
59 result = Rectangle(rectList[0], rectList[1], rectList[2], rectList[3]);
60 } else if (value->hasValue(u8"position") != value->hasValue(u8"size")) {
61 exitWithErrorInValue(value, "Only 'position' or 'size' is not allowed.");
62 } else if (value->hasValue(u8"position") && value->hasValue(u8"size")) {
63 auto posList = value->getListOrThrow<int>(u8"position");
64 if (posList.size() != 2) { exitWithErrorInValue(value, "Position must have exactly two elements."); }
65 auto sizeList = value->getListOrThrow<int>(u8"size");
66 if (sizeList.size() != 2) { exitWithErrorInValue(value, "Size must have exactly two elements."); }
67 result = Rectangle(Position{posList[0], posList[1]}, Size(sizeList[0], sizeList[1]));
68 } else {
69 result = Rectangle{value->getOrThrow<int>(u8"x"), value->getOrThrow<int>(u8"y"),
70 value->getOrThrow<int>(u8"width"), value->getOrThrow<int>(u8"height")};
71 }
72 return result;
73 }
74
75 [[nodiscard]] auto buildWorld() const -> World {
76 try {
77 World world;
78 for (const auto &roomValue : *config->getSectionListOrThrow("field.room")) {
79 const auto roomRect = rectFromSection(roomValue);
80 world.field.addRoom(roomRect);
81 }
82 if (!cMinimumFieldSize.fitsInto(world.field.rect.size)) {
83 std::cerr << std::format("Field size must be at least {}x{}\n", cMinimumFieldSize.width, cMinimumFieldSize.height);
84 exit(1);
85 }
86 if (!world.field.rect.size.fitsInto(cMaximumFieldSize)) {
87 std::cerr << std::format("Field size must be at most {}x{}\n", cMaximumFieldSize.width, cMaximumFieldSize.height);
88 exit(1);
89 }
90 world.addExitAtRandomPosition();
91 world.setPlayerToRandomPosition();
92 for (int i = 0; i < 3; ++i) {
93 world.addRobotAtRandomPosition();
94 }
95 return world;
96 } catch (const Error &error) {
97 std::cerr << error.toText().toCharString() << "\n";
98 exit(1);
99 }
100 }
Download the updated Application.hpp
Testing for Values
Before using a configured value, it’s important to verify whether it was actually set. You can do this using:
value->hasValue(u8"rectangle")
This method returns true
if a value exists for the given key. Keep in mind, however, that the value could be of any type, including a section. So you’ll still need to validate the type afterward.
54 [[nodiscard]] static auto rectFromSection(const ValuePtr &value) -> Rectangle {
55 Rectangle result;
56 if (value->hasValue(u8"rectangle")) {
57 const auto rectList = value->getListOrThrow<int>(u8"rectangle");
58 if (rectList.size() != 4) { exitWithErrorInValue(value, "Rectangle must have exactly four elements."); }
59 result = Rectangle(rectList[0], rectList[1], rectList[2], rectList[3]);
60 } else if (value->hasValue(u8"position") != value->hasValue(u8"size")) {
61 exitWithErrorInValue(value, "Only 'position' or 'size' is not allowed.");
62 } else if (value->hasValue(u8"position") && value->hasValue(u8"size")) {
63 auto posList = value->getListOrThrow<int>(u8"position");
64 if (posList.size() != 2) { exitWithErrorInValue(value, "Position must have exactly two elements."); }
65 auto sizeList = value->getListOrThrow<int>(u8"size");
66 if (sizeList.size() != 2) { exitWithErrorInValue(value, "Size must have exactly two elements."); }
67 result = Rectangle(Position{posList[0], posList[1]}, Size(sizeList[0], sizeList[1]));
68 } else {
69 result = Rectangle{value->getOrThrow<int>(u8"x"), value->getOrThrow<int>(u8"y"),
70 value->getOrThrow<int>(u8"width"), value->getOrThrow<int>(u8"height")};
71 }
72 return result;
Reading a Value List
To retrieve a list of values from the configuration, use the following method:
value->getListOrThrow<int>(u8"rectangle")
54 [[nodiscard]] static auto rectFromSection(const ValuePtr &value) -> Rectangle {
55 Rectangle result;
56 if (value->hasValue(u8"rectangle")) {
57 const auto rectList = value->getListOrThrow<int>(u8"rectangle");
58 if (rectList.size() != 4) { exitWithErrorInValue(value, "Rectangle must have exactly four elements."); }
59 result = Rectangle(rectList[0], rectList[1], rectList[2], rectList[3]);
60 } else if (value->hasValue(u8"position") != value->hasValue(u8"size")) {
61 exitWithErrorInValue(value, "Only 'position' or 'size' is not allowed.");
62 } else if (value->hasValue(u8"position") && value->hasValue(u8"size")) {
63 auto posList = value->getListOrThrow<int>(u8"position");
64 if (posList.size() != 2) { exitWithErrorInValue(value, "Position must have exactly two elements."); }
65 auto sizeList = value->getListOrThrow<int>(u8"size");
66 if (sizeList.size() != 2) { exitWithErrorInValue(value, "Size must have exactly two elements."); }
67 result = Rectangle(Position{posList[0], posList[1]}, Size(sizeList[0], sizeList[1]));
68 } else {
69 result = Rectangle{value->getOrThrow<int>(u8"x"), value->getOrThrow<int>(u8"y"),
70 value->getOrThrow<int>(u8"width"), value->getOrThrow<int>(u8"height")};
71 }
72 return result;
The getListOrThrow<>
template method is a convenient all-in-one call. It first checks whether the value exists at the given location. Then, it verifies whether the value is a list and ensures that each entry matches the expected type.
In this case, we expect a list of integers. If all values in the list are valid and convertible to int
, they are returned as a C++ vector. If any value is invalid—be it due to type mismatch or exceeding the int
range—an exception is thrown with a clear, helpful error message.
Give it a try and see how it behaves!
Reading Integers with a Cast
Previously, individual values like x
were read using:
static_cast<int>(roomValue->getIntegerOrThrow(u8"x"))
We’ve now switched to this cleaner syntax:
value->getOrThrow<int>(u8"x")
54 [[nodiscard]] static auto rectFromSection(const ValuePtr &value) -> Rectangle {
55 Rectangle result;
56 if (value->hasValue(u8"rectangle")) {
57 const auto rectList = value->getListOrThrow<int>(u8"rectangle");
58 if (rectList.size() != 4) { exitWithErrorInValue(value, "Rectangle must have exactly four elements."); }
59 result = Rectangle(rectList[0], rectList[1], rectList[2], rectList[3]);
60 } else if (value->hasValue(u8"position") != value->hasValue(u8"size")) {
61 exitWithErrorInValue(value, "Only 'position' or 'size' is not allowed.");
62 } else if (value->hasValue(u8"position") && value->hasValue(u8"size")) {
63 auto posList = value->getListOrThrow<int>(u8"position");
64 if (posList.size() != 2) { exitWithErrorInValue(value, "Position must have exactly two elements."); }
65 auto sizeList = value->getListOrThrow<int>(u8"size");
66 if (sizeList.size() != 2) { exitWithErrorInValue(value, "Size must have exactly two elements."); }
67 result = Rectangle(Position{posList[0], posList[1]}, Size(sizeList[0], sizeList[1]));
68 } else {
69 result = Rectangle{value->getOrThrow<int>(u8"x"), value->getOrThrow<int>(u8"y"),
70 value->getOrThrow<int>(u8"width"), value->getOrThrow<int>(u8"height")};
71 }
72 return result;
Not only is this new version more concise, but it also adds an important benefit: range checking. getOrThrow<>
ensures that the retrieved value fits within the target type—in this case, int
. If it doesn’t, it throws an exception with a descriptive message.
You can test this by deliberately entering an out-of-range value like x = 0x7000'0000'0000'0000
. The application will now reject this gracefully, whereas previously it may have defaulted silently to zero.
Compile and Run the Updated Game
With our improved configuration logic in place, it’s time to compile and run the updated game.
$ cmake --build cmake-build
$ ./cmake-build/robot-escape/robot-escape configuration.elcl
----------------------------==[ ROBOT ESCAPE ]==----------------------------- Welcome to Robot Escape! You (☻) must run to the exit (⚑) before any robot (♟) catches you. ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░┏━━━━━━━━━━━━━━━━┓░░░░░░░░ ░░░░░░┃ ┃░░░░░░░░ ░░░░┏━┛ ┃░░░░░░░░ ░░░░┃ ☻ ┃░░░░░░░░ ░░░░┃ ┏━━┓ ┃░░░░░░░░ ░░░░┗━┓ ┃░░┃ ┃░░░░░░░░ ░░░░░░┃ ┃░░┃ ┗━━━━┓░░░ ░░░░░░┃ ♟ ┃░░┃ ♟┃░░░ ░░░░░░┃ ┃░░┃ ┏━━━━┛░░░ ░░░░░░┃ ┗━━┛⚑ ┃░░░░░░░░ ░░░░░░┃ ┗━━┓░░░░░ ░░░░░░┃ ♟ ┃░░░░░ ░░░░░░┃ ┃░░░░░ ░░░░░░┗━━━━━━━━━━━━━━━┓ ┃░░░░░ ░░░░░░░░░░░░░░░░░░░░░░┗━━━┛░░░░░ Enter your move (n/e/s/w/q=quit): e (...) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░┏━━━━━━━━━━━━━━━━┓░░░░░░░░ ░░░░░░┃ ┃░░░░░░░░ ░░░░┏━┛ ┃░░░░░░░░ ░░░░┃ ♟∙∙☻ ┃░░░░░░░░ ░░░░┃ ∙┏━━┓ ┃░░░░░░░░ ░░░░┗━┓ ∙∙┃░░┃ ♟┃░░░░░░░░ ░░░░░░┃ ♟┃░░┃ ∙┗━━━━┓░░░ ░░░░░░┃ ∙∙┃░░┃ ∙∙ ┃░░░ ░░░░░░┃ ∙ ┃░░┃ ┏━━━━┛░░░ ░░░░░░┃ ┗━━┛⚑ ┃░░░░░░░░ ░░░░░░┃ ┗━━┓░░░░░ ░░░░░░┃ ┃░░░░░ ░░░░░░┃ ┃░░░░░ ░░░░░░┗━━━━━━━━━━━━━━━┓ ┃░░░░░ ░░░░░░░░░░░░░░░░░░░░░░┗━━━┛░░░░░ Enter your move (n/e/s/w/q=quit): e (...) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░┏━━━━━━━━━━━━━━━━┓░░░░░░░░ ░░░░░░┃ ┃░░░░░░░░ ░░░░┏━┛ ┃░░░░░░░░ ░░░░┃ ┃░░░░░░░░ ░░░░┃ ┏━━┓ ∙ ┃░░░░░░░░ ░░░░┗━┓ ┃░░┃ ∙ ┃░░░░░░░░ ░░░░░░┃ ┃░░┃ ∙ ┗━━━━┓░░░ ░░░░░░┃ ∙┃░░┃ ♟∙ ┃░░░ ░░░░░░┃ ∙┃░░┃ ∙∙ ┏━━━━┛░░░ ░░░░░░┃ ♟∙┗━━┛☻♟ ┃░░░░░░░░ ░░░░░░┃ ┗━━┓░░░░░ ░░░░░░┃ ┃░░░░░ ░░░░░░┃ ┃░░░░░ ░░░░░░┗━━━━━━━━━━━━━━━┓ ┃░░░░░ ░░░░░░░░░░░░░░░░░░░░░░┗━━━┛░░░░░ You won!
Recap
Nice work! You’ve just made your configuration format much more flexible and developer-friendly.
Here’s a quick summary of what you accomplished:
Introduced grouped value lists like
rectangle
,position
, andsize
to simplify the configuration syntax.Enhanced the configuration parser to accept and validate these new input formats using
rectFromSection()
.Learned how to check for values and read them safely using
hasValue()
,getListOrThrow<>
, andgetOrThrow<>
.Replaced manual
static_cast
calls with cleaner, type-safe conversions that also validate ranges.