// 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 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 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::vector 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 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)"); }