Last Update:
Spaceship Generator for May the 4th in C++ - Results!
Table of Contents
Two weeks ago, I announced a little game on my blog! Today I’d like to present some of the solutions you sent me and discuss a few things from various aspects of Modern C++.
Big Thanks!
First of all, I’d like to thank all of the participants for sending the solutions. I got 14 of them.
Although the task might sound easy, it required between 100 or 300 lines of code. So it wasn’t just a five-minute coding session. Thanks for your time and I hope it was a funny experience for you :)
Rules Reminder
The task for the game was as follows: write a random spaceship generator which can create amazing spaceships (*)!
(*) Not to mistake with the spaceship operator for C++20 :)
For example:
Spaceship with ionizing engine, small wings, regular cabin, tie fighter style fuselage and rocket launcher weapon.
Prizes
Each participant got a chance to win the following reward:
3-month Educative.io service and 15$ Amazon.com Gift Card
I have 5 of those “packs” for five people.
The winners were selected randomly from all of the participants and should already get notifications.
The Starting Legacy Code
Please have a look at my initial example :)
#include <string>
#include <cstring>
#include <iostream>
#include <vector>
#include <fstream>
#include <random>
#include <algorithm>
char partsFileName[128] = "vehicle_parts.txt";
std::vector<std::string> allParts;
class Spaceship {
public:
static void GenerateShip(SpaceShip *pOutShip);
void Print() {
// print code...
}
private:
std::string _engine;
std::string _fuselage;
std::string _cabin;
std::string _large_wings; // optional
std::string _small_wings; // optional
std::string _armor;
std::string _weapons[4]; // max weapon count is 4
};
void Spaceship::GenerateShip(Spaceship *pOutShip) {
std::vector<std::string> engineParts;
std::vector<std::string> fuselageParts;
std::vector<std::string> cabinParts;
std::vector<std::string> wingsParts;
std::vector<std::string> armorParts;
std::vector<std::string> weaponParts;
for (const auto& str : allParts) {
if (str.rfind("engine") != std::string::npos)
engineParts.push_back(str);
else if (str.rfind("fuselage") != std::string::npos)
fuselageParts.push_back(str);
else if (str.rfind("cabin") != std::string::npos)
cabinParts.push_back(str);
else if (str.rfind("wings") != std::string::npos)
wingsParts.push_back(str);
else if (str.rfind("armor") != std::string::npos)
armorParts.push_back(str);
else if (str.rfind("weapon") != std::string::npos)
weaponParts.push_back(str);
}
std::random_device rd;
std::mt19937 g(rd());
std::shuffle(engineParts.begin(), engineParts.end(), g);
std::shuffle(fuselageParts.begin(), fuselageParts.end(), g);
std::shuffle(cabinParts.begin(), cabinParts.end(), g);
std::shuffle(wingsParts.begin(), wingsParts.end(), g);
std::shuffle(armorParts.begin(), armorParts.end(), g);
std::shuffle(weaponParts.begin(), weaponParts.end(), g);
// select parts:
pOutShip->_engine = engineParts[0];
pOutShip->_fuselage = fuselageParts[0];
pOutShip->_cabin = cabinParts[0];
pOutShip->_armor = armorParts[0];
pOutShip->_large_wings = wingsParts[0];
pOutShip->_weapons[0] = weaponParts[0];
}
int main(int argc, char* argv[]) {
if (argc > 1) {
strcpy(partsFileName, argv[1]);
}
std::cout << "parts loaded from: " << partsFileName << '\n';
std::ifstream file(partsFileName);
if (file.is_open()) {
std::string line;
while (std::getline(file, line)) {
allParts.push_back(line);
}
file.close();
}
Spaceship sp;
Spaceship::GenerateShip(&sp);
sp.Print();
}
As you can see above the program consists of several parts:
- It reads all the lines from a given file and stores it in a global vector of strings. Yes… global, as it’s the best way to program such programs :)
- Of course almost no error checking needed :)
- Then we define a
Spaceship
with the best possible namesp
. - Later the spaceship is passed to a generator function that does the main job:
- It sorts the input parts and groups them into separate containers.
- Then it shuffles the parts containers
- We can then use the first objects in those containers and assign them to the appropriate member variables of the output spaceship
- At the end, the main function invokes a print member function that shows the generated spaceship.
Can you write better code? :)
Yes, you can! Through your submissions, you managed to fix all of my bad patterns :)
Some Cool Ideas
Here are the code samples extracted from submissions.
Getting rid of global variables
First of all, my super-advanced starting code example used global variables. The submitted code nicely fixed this issue by using only local variables.
For example in solution from Thomas H. there’s a separate class that contains all parts, this is a small Database:
PartDB partDB = readPartDB(partsFileName);
const Spaceship sp = makeRandomSpaceShip(partDB);
And the details:
struct PartDB {
std::vector<Engine> engines;
std::vector<Fuselage> fuselages;
std::vector<Cabin> cabins;
std::vector<Armor> armors;
std::vector<Wing> wings;
std::vector<Weapon> weapons;
std::vector<Shield> shields;
};
PartDB readPartDB(const std::filesystem::path& partsFileName) {
PartDB partDB;
std::ifstream file(partsFileName);
if (file.is_open()) {
std::string line;
while (std::getline(file, line)) {
if (line.rfind("engine") != std::string::npos) {
partDB.engines.push_back(Engine{line});
} else if (line.rfind("fuselage") != std::string::npos) {
// ...
} else {
std::cerr << "Unknown part: '" << line << " '\n";
}
}
}
return partDB;
}
This is nice and a simple way to keep all the parts in one place. My starting code mixed loading with the generation, so it was not the best pattern.
Clever way of loading data
In my starting code I used only a vector of strings to store all the parts. But many solutions improved that by using maps and even maps of variants:
void GetDataFromFile()
{
std::string line;
inputData.exceptions(std::ifstream::badbit);
while (std::getline(inputData, line))
{
int n = line.rfind(" ");
std::array<std::string, 2> arStrParts{ line.substr(0, n), line.substr(n + 1) };
if (auto it = umShipParts.find(arStrParts[1]); it != umShipParts.end())
{
std::visit([&arStrParts](auto& obj) { obj.add(arStrParts[0]); }, umShipParts[arStrParts[1]]);
}
}
}
More in the full solution from Mike @Wandbox
Another cool example we can find in the code created by Marius Bancila:
part_type find_part_type(std::string_view description)
{
static std::vector<part_description> parts
{
{part_type::engine, {"engine"}},
{part_type::fuselage,{"fuselage"}},
{part_type::cabin, {"cabin"}},
{part_type::wings, {"wings"}},
{part_type::armor, {"armor", "shield"}},
{part_type::weapon, {"weapon"}}
};
for (auto const & [type, desc] : parts)
{
for (auto const& d : desc)
{
if (description.rfind(d) != std::string::npos)
return type;
}
}
throw std::runtime_error("unknown part");
}
In the above examples, you can see that we have much better code, more readable and scalable (if you want to add new types of parts).
Getting more flexibility
In the another solution Michal stored also the names of the parts:
for (auto&& partsLine : partLines)
{
auto key = utils::last_word(partsLine);
auto part = partsLine.substr(0, partsLine.size() - key.size() - 1);
auto keyIt = parts.find(key);
if (keyIt == parts.end())
{
parts.try_emplace(std::move(key), std::vector<std::string> {std::move(part)});
}
else
{
parts.at(key).emplace_back(std::move(part));
}
}
This approach allows to specify the mandatory parts in a just an array, without creating the types for each part:
constexpr auto mandatoryParts = {"engine"sv, "fuselage"sv, "cabin"sv, "armor"sv};
Have a look @Wandbox
Getting the full flexibility
Also, I’d like to draw your attention to the example sent by JFT who went even further with the flexibility. Rather than fixing the specification of the spaceship in code, he described it in the parts file.
That way, the design of the spaceship is fully customisable, and there’s no need to change code of the application. What’s more, the author managed to write quite concise code, so it’s quite short:
Example of a spaceship design:
1 engine
1 fuselage
1 cabin
1 armor
-4 weapon
-1 wings_s
-1 wings_l
-1 shield
where:
where number_required is:
0 to ignore
> 0 for required up to
< 0 for optional up to
The code is available here @Wandbox
Pain with enums
In a few examples I’ve noticed the following code:
enum class spaceshipPartsEnum
{
engine,
fuselage,
cabin,
wings,
armor,
weapon
};
And then the tostring()
method.
std::string enum_to_string (spaceshipPartsEnum part)
{
switch (part)
{
case spaceshipPartsEnum::engine:
return "engine";
case spaceshipPartsEnum::fuselage:
return "fuselage";
case spaceshipPartsEnum::cabin:
return "cabin";
case spaceshipPartsEnum::wings:
return "wings";
case spaceshipPartsEnum::armor:
return "armor";
case spaceshipPartsEnum::weapon:
return "weapon";
}
assert (false);
return {};
}
It would be great to have native support for enum to string conversions!
Useful utils
From Michal: See @Wandbox
namespace utils
{
/**
* Just a siple wrapper of random nuber generator.
*/
class random_uniform_int
{
private:
std::mt19937 generator_;
std::uniform_int_distribution<size_t> distribution_;
public:
random_uniform_int(size_t const min, size_t const max, unsigned long const seed) :
generator_ {seed},
distribution_ {min, max}
{
}
auto next_index () -> size_t
{
return distribution_(generator_);
}
};
/**
* Just a siple wrapper of random nuber generator.
*/
class random_bool
{
private:
std::mt19937 generator_;
std::uniform_real_distribution<double> distribution_;
public:
random_bool(unsigned long const seed) :
generator_ {seed},
distribution_ {0, 1}
{
}
auto next_bool () -> bool
{
return distribution_(generator_) < 0.5;
}
};
auto last_word (const std::string& s) -> std::string
{
auto const lastSpaceIndex = s.rfind(' ');
if (lastSpaceIndex == std::string::npos)
{
return "";
}
return s.substr(lastSpaceIndex + 1);
}
}
C++20 Parts
I guess that one of the easiest features that you could use from C++20 is starts_with
or ends_with
member functions that we get for string and string views: In the example from Benjamin he used it to replace rfind()
calls:
Have a look @Wandbox
Warehouse& Warehouse::add(std::string description) {
if (description.ends_with("engine")) {
engines_.emplace_back(std::move(description));
} else if (description.ends_with("fuselage")) {
fuselage_.emplace_back(std::move(description));
// ...
And if you’d like to see more of C++, have a look at this code from Jackson @Wandbox. He used ranges and concepts and also…
And also one of the coolest use of the spaceship operator:
// Spaceship for the Spaceship 🙂
auto operator<=>(const Spaceship& other) const noexcept = default;
Summary
One again thank you for all the solutions! In this short blog post, I managed to extract only a few bits of code, but there’s more to that. Your solutions even got part validation, logging, template machinery and much more cool ideas!
I've prepared a valuable bonus for you!
Learn all major features of recent C++ Standards on my Reference Cards!
Check it out here: