207 lines
7.6 KiB
C++
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)");
|
|
}
|