sots-engine/tests/game_design/test_structure.cpp

207 lines
7.6 KiB
C++

// A* structure rules on the mini catalog.
#include "game/design/rules.h"
#include "mini_catalog.h"
#include "test_main.h"
using namespace game::data;
using namespace game::design;
TEST(mini_catalog_loaded) {
const Catalog& c = mini_catalog();
CHECK_EQ(c.races.size(), std::size_t(4));
CHECK(c.section("Human", "DEArmor") != nullptr);
CHECK(c.section_by_id("Human", 37) == c.section("Human", "DEArmor"));
CHECK(c.weapon_by_id(8) == c.weapon("bal_gauss"));
CHECK_EQ(c.turrets.size(), std::size_t(19));
CHECK(c.tech.find("IND_CruisCon") != nullptr);
}
TEST(stock_armor_is_valid) {
std::vector<Violation> v = mini_rules().validate(armor());
CHECK_EQ(v.size(), std::size_t(0));
for (const Violation& x : v) std::printf(" %s\n", x.str().c_str());
CHECK_EQ(join(error_rules(armor(Species::Tarkas))), "");
}
TEST(resolution_by_id_species_and_file_name) {
Design d = armor();
d.race = Species::Unknown; // race comes from the first filled slot
d.command().species = Species::Human;
d.command().section_id = 42;
d.command().section.clear();
d.mission().section = "Species/Human/sections/DEArmor.shipsection";
d.engine().section = "defission";
d.command().weapons = {WeaponRef::by_id(8)};
d.mission().weapons = {WeaponRef::by_file("Weapons/bal_gauss.weapon"), WeaponRef::by_file("bal_gauss.weapon"),
WeaponRef::by_stem("MIS")};
CHECK(d.effective_race() == Species::Human);
CHECK_EQ(join(error_rules(d)), "");
const Ruleset& r = mini_rules();
CHECK(r.resolve_section(Species::Human, d.command()) == mini_catalog().section("Human", "DECommand"));
CHECK(r.resolve_weapon(WeaponRef::by_id(999)) == nullptr);
CHECK(r.resolve_section(Species::Human, use("Nope")) == nullptr);
SectionUse bad_id;
bad_id.section_id = 999;
CHECK(r.resolve_section(Species::Human, bad_id) == nullptr);
}
TEST(A1_reserved_slots_must_be_empty) {
Design d = armor();
d.slot(Slot::Reserved3) = use("DECommand", {"bal_gauss"});
CHECK_EQ(join(error_rules(d)), "A1");
}
TEST(A2_mission_required) {
Design d = armor();
d.mission() = SectionUse{};
CHECK_EQ(join(error_rules(d)), "A2");
// an unresolvable mission section stops at A5 (nothing else can be judged)
d.mission() = use("DENowhere");
CHECK_EQ(join(error_rules(d)), "A5");
}
TEST(A3_hull_needs_command_and_engine) {
Design d = armor();
d.engine() = SectionUse{};
CHECK_EQ(join(error_rules(d)), "A3");
d.command() = SectionUse{};
std::vector<Violation> v = mini_rules().validate(d);
int a3 = 0;
for (const Violation& x : v)
if (x.rule == "A3") ++a3;
CHECK_EQ(a3, 2);
}
TEST(A3_standalone_cannot_take_partners) {
Design d = armor();
d.mission() = use("DEDefencePlatform", {"bal_gauss", "bal_gauss", "mis"});
CHECK_EQ(join(error_rules(d)), "A3");
d.command() = SectionUse{};
d.engine() = SectionUse{};
CHECK_EQ(join(error_rules(d)), "");
// even an unresolvable partner counts as "filled"
d.command() = use("DENowhere");
CHECK_EQ(join(error_rules(d)), "A3,A5");
}
TEST(A4_slot_must_match_section_type) {
Design d = armor();
d.command() = use("DEArmor", {"bal_gauss", "bal_gauss", "mis"});
CHECK_EQ(join(error_rules(d)), "A4");
d = armor();
d.engine() = use("DECommand", {"bal_gauss"});
CHECK_EQ(join(error_rules(d)), "A4");
}
TEST(A4_typeless_rider_hull) {
// hidden default rider design: [0,0] _AssaultShuttle [0,0] -> info only
Design d;
d.race = Species::Human;
d.hidden = true;
d.mission() = use("_AssaultShuttle");
d.known_techs = std::vector<std::string>{};
std::vector<Violation> v = mini_rules().validate(d);
CHECK_EQ(join(rules_at(v, Level::Error)), "");
CHECK_EQ(join(rules_at(v, Level::Info)), "A4");
// not hidden but design_class rider: still info
d.hidden = false;
CHECK_EQ(join(rules_of(d, Level::Info)), "A4");
CHECK_EQ(join(error_rules(d)), "");
// a typeless NPC hull placed in the command slot is an error (and _Herald is a cruiser: A6 too)
Design n;
n.race = Species::NPC;
n.command() = use("_Herald", {"", "", ""});
n.mission() = use("_DEMission", {"bal_gauss", "bal_gauss", "mis"});
n.engine() = use("_DEEngine", {"bal_gauss"});
CHECK_EQ(join(error_rules(n)), "A4,A6");
}
TEST(A5_one_species_catalog) {
Design d = armor();
d.race = Species::Unknown;
CHECK_EQ(join(error_rules(d)), "A5"); // no filled slot names a species
d = armor(Species::Liir); // no Liir catalog in the mini root
CHECK_EQ(join(error_rules(d)), "A5");
d = armor(Species::Zuul);
d.known_techs.reset();
d.mission() = use("DEDefencePlatform"); // Zuul has no DEDefencePlatform
CHECK_EQ(join(error_rules(d)), "A5");
d = armor();
d.engine().species = Species::Tarkas; // mixed species record
CHECK_EQ(join(error_rules(d)), "A3,A5");
}
TEST(A6_class_homogeneous) {
Design d = armor();
d.known_techs.reset();
d.engine() = use("CRFission", {"bal_gauss"});
CHECK_EQ(join(error_rules(d)), "A6");
Design cr;
cr.race = Species::Human;
cr.command() = use("CRCommand", {"bal_gauss"});
cr.mission() = use("CRArmor", {"bal_gauss"});
cr.engine() = use("CRFission", {"bal_gauss"});
CHECK_EQ(join(error_rules(cr)), "");
}
TEST(A7_exclude_lists) {
Design d = armor();
d.command() = use("DERamScoop", {"bal_gauss"});
std::vector<Violation> v = mini_rules().validate(d);
CHECK_EQ(join(rules_at(v, Level::Error)), "A7");
bool at_command = false;
for (const Violation& x : v)
if (x.rule == "A7" && x.slot && *x.slot == Slot::Command) at_command = true;
CHECK(at_command);
}
TEST(A8_station_triples) {
Design d;
d.race = Species::Human;
d.command() = use("DNStation_Fore", {"bal_gauss"});
d.mission() = use("DNStation", {"bal_gauss"});
d.engine() = use("DNStation_Aft", {"bal_gauss"});
CHECK_EQ(join(error_rules(d)), "");
d.command() = use("DNCommand", {"bal_gauss"});
CHECK_EQ(join(error_rules(d)), "A8");
d.command() = SectionUse{};
CHECK_EQ(join(error_rules(d)), "A8");
// the explicit_section halves cannot serve another mission section
Design e;
e.race = Species::Human;
e.command() = use("DNStation_Fore", {"bal_gauss"});
e.mission() = use("CRArmor", {"bal_gauss"});
e.engine() = use("CRFission", {"bal_gauss"});
CHECK_EQ(join(error_rules(e)), "A6,A8");
}
TEST(A9_nodesign_is_a_warning) {
Design n;
n.race = Species::NPC;
n.command() = use("_DECommand", {"bal_gauss"});
n.mission() = use("_DEMission", {"bal_gauss", "bal_gauss", "mis"});
n.engine() = use("_DEEngine", {"bal_gauss"});
CHECK_EQ(join(error_rules(n)), "");
CHECK_EQ(join(rules_of(n, Level::Warn)), "A9");
n.hidden = true;
CHECK_EQ(join(rules_of(n, Level::Warn)), "");
}
TEST(sockets_are_geometry_not_a_key) {
// DECommand socket_aft CommandNode + DEArmor socket_fore MissionNode01: names differ, design is fine
const Catalog& c = mini_catalog();
CHECK_EQ(c.section("Human", "DECommand")->socket_aft, "CommandNode");
CHECK_EQ(c.section("Human", "DEArmor")->socket_fore, "MissionNode01");
CHECK_EQ(join(error_rules(armor())), "");
}
TEST(violation_formatting) {
Violation v{"B4", Level::Error, "does not fit", Slot::Mission, 2};
CHECK_EQ(v.str(), "ERROR B4 [mission bank 2]: does not fit");
Violation w{"A6", Level::Warn, "mixed", std::nullopt, std::nullopt};
CHECK_EQ(w.str(), "WARN A6: mixed");
CHECK(parse_slot("Engine") == Slot::Engine);
CHECK(!parse_slot("slot3"));
CHECK_EQ(WeaponRef::by_id(8).describe(), "#8");
CHECK_EQ(WeaponRef::none().describe(), "(none)");
}