// C* tech gating and D* option rules; applied_techs (the save's DOpts). #include "game/design/rules.h" #include "mini_catalog.h" #include "test_main.h" using namespace game::data; using namespace game::design; namespace { Design with_techs(Design d, std::vector techs) { d.known_techs = std::move(techs); return d; } } // namespace TEST(C1_section_requires) { CHECK_EQ(join(error_rules(with_techs(armor(), {"WEP_GsDrvr", "DRV_Node"}))), "C1"); CHECK_EQ(join(error_rules(with_techs(armor(), {"WEP_GsDrvr", "DRV_Fissn"}))), "C1"); CHECK_EQ(join(error_rules(with_techs(armor(), {"wep_gsdrvr", "drv_fissn", "DRV_NODE"}))), ""); // case-insensitive Design d = armor(); d.known_techs.reset(); // no player information: gating skipped entirely CHECK_EQ(join(error_rules(d)), ""); } TEST(C1_group_requirements) { Design d = armor(); d.mission() = use("DETorpedo", {"trp_photon", "mis"}); d.known_techs = std::vector{"DRV_Fissn", "DRV_Node", "WEP_GsDrvr", "WEP_PlasTrp"}; CHECK_EQ(join(error_rules(d)), "C2"); // GRP_Torps satisfied by WEP_PlasTrp; trp_photon itself needs WEP_PhotTrp d.known_techs->push_back("WEP_PhotTrp"); CHECK_EQ(join(error_rules(d)), ""); d.known_techs = std::vector{"DRV_Fissn", "DRV_Node", "WEP_GsDrvr", "WEP_PhotTrp"}; CHECK_EQ(join(error_rules(d)), ""); TechSet none({"WEP_RedLas"}); CHECK(!none.satisfies(mini_catalog().tech, "GRP_Torps")); CHECK(!none.satisfies(mini_catalog().tech, "GRP_NoSuchGroup")); CHECK(TechSet({"WEP_HvyLancer"}).satisfies(mini_catalog().tech, "grp_hvybeam")); } TEST(C1_unknown_tech_is_a_warning) { Design d = armor(); d.engine() = use("DEOddTech", {"bal_gauss"}); // DEOddTech requires DRV_Fissn (known) and XXX_Nowhere (not in the tree): // a token the tree cannot satisfy is reported, never an error. std::vector v = mini_rules().validate(d); CHECK_EQ(join(rules_at(v, Level::Error)), ""); CHECK_EQ(join(rules_at(v, Level::Warn)), "C1"); } TEST(C2_weapon_requires) { CHECK_EQ(join(error_rules(with_techs(armor(), {"DRV_Fissn", "DRV_Node"}))), "C2"); Design d = armor(); d.mission() = use("DEArmor", {"bal_gauss", "las_red", "mis"}); CHECK_EQ(join(error_rules(d)), "C2"); d.known_techs->push_back("WEP_RedLas"); CHECK_EQ(join(error_rules(d)), ""); } TEST(C3_options_must_be_researched) { Design d = armor(); d.mission() = use("DEArmor", {"bal_gauss", "bal_gauss", "mis"}, {"IND_PlyAlloy"}); CHECK_EQ(join(error_rules(d)), "C3"); d.known_techs->push_back("IND_PlyAlloy"); CHECK_EQ(join(error_rules(d)), ""); } TEST(C4_hull_class_tech) { const Catalog& c = mini_catalog(); CHECK(hull_class_tech(*c.section("Human", "DEArmor")) == std::nullopt); CHECK(*hull_class_tech(*c.section("Human", "CRArmor")) == "IND_CruisCon"); CHECK(*hull_class_tech(*c.section("Human", "DNCommand")) == "IND_DreadCon"); CHECK(hull_class_tech(*c.section("Human", "DNStation")) == std::nullopt); // station exemption CHECK(hull_class_tech(*c.section("Human", "_AssaultShuttle")) == std::nullopt); Design cr; cr.race = Species::Human; cr.command() = use("CRCommand", {"bal_gauss"}); cr.mission() = use("CRArmor", {"bal_gauss"}); cr.engine() = use("CRFission", {"bal_gauss"}); cr.known_techs = std::vector{"DRV_Fissn", "DRV_Node", "WEP_GsDrvr"}; std::vector v = mini_rules().validate(cr); CHECK_EQ(join(rules_at(v, Level::Error)), "C4"); int n = 0; for (const Violation& x : v) if (x.rule == "C4") ++n; CHECK_EQ(n, 3); cr.known_techs->push_back("IND_CruisCon"); CHECK_EQ(join(error_rules(cr)), ""); // station: IND_OrbCom is the gate, no IND_DreadCon needed Design st; st.race = Species::Human; st.command() = use("DNStation_Fore", {"bal_gauss"}); st.mission() = use("DNStation", {"bal_gauss"}); st.engine() = use("DNStation_Aft", {"bal_gauss"}); st.known_techs = std::vector{"WEP_GsDrvr", "IND_OrbCom"}; CHECK_EQ(join(error_rules(st)), ""); } TEST(C5_race_can_never_research) { const Ruleset& r = mini_rules(); CHECK(r.unobtainable("DRV_Hyper", Species::Human)); CHECK(!r.unobtainable("DRV_Hyper", Species::Tarkas)); CHECK(r.unobtainable("DRV_Node", Species::Tarkas)); CHECK(!r.unobtainable("DRV_Node", Species::Human)); CHECK(!r.unobtainable("DRV_Node", Species::NPC)); // never written -> engine default 100 CHECK(!r.unobtainable("DRV_Fissn", Species::Human)); CHECK(!r.unobtainable("NoSuchTech", Species::Human)); Design d = armor(); d.engine() = use("DEHyperTest", {"bal_gauss"}); d.known_techs = std::vector{"DRV_Fissn", "DRV_Hyper", "WEP_GsDrvr"}; CHECK_EQ(join(error_rules(d)), "C5"); // researched or not, Human can never have it CHECK_EQ(join(error_rules(armor(Species::Tarkas))), ""); } TEST(hidden_designs_gate_at_warn_level) { // the Tarkas default assault shuttle exists on turn 1 with DRN_AdvFrm unresearched Design d; d.race = Species::Tarkas; d.hidden = true; d.mission() = use("_AssaultShuttle"); d.known_techs = std::vector{"DRV_Fissn"}; std::vector v = mini_rules().validate(d); CHECK_EQ(join(rules_at(v, Level::Error)), ""); CHECK_EQ(join(rules_at(v, Level::Warn)), "C1"); CHECK_EQ(join(rules_at(v, Level::Info)), "A4"); d.hidden = false; CHECK_EQ(join(error_rules(d)), "C1"); // C2/C3/C4 downgrade too Design cr; cr.race = Species::Human; cr.hidden = true; cr.command() = use("CRCommand", {"bal_gauss"}); cr.mission() = use("CRArmor", {"las_red"}, {"IND_PlyAlloy"}); cr.engine() = use("CRFission", {"bal_gauss"}); cr.known_techs = std::vector{}; v = mini_rules().validate(cr); CHECK_EQ(join(rules_at(v, Level::Error)), "D1"); // IND_PlyAlloy is not an option of CRArmor: still an error CHECK_EQ(join(rules_at(v, Level::Warn)), "C1,C2,C3,C4"); } TEST(D1_options_one_per_group_members_only) { Design d = armor(); d.known_techs = std::vector{"DRV_Fissn", "DRV_Node", "WEP_GsDrvr", "IND_PlyAlloy", "IND_MagLat", "SLD_MkOne", "SLD_MkTwo", "IND_RefCoat", "DRV_RecFiss"}; d.mission() = use("DEArmor", {"bal_gauss", "bal_gauss", "mis"}, {"IND_PlyAlloy", "IND_MagLat"}); CHECK_EQ(join(error_rules(d)), "D1"); d.mission().options = {"DRV_RecFiss"}; // an option of the engine, not of DEArmor CHECK_EQ(join(error_rules(d)), "D1"); d.mission().options = {"IND_MagLat", "IND_RefCoat", "SLD_MkOne"}; // one per group incl. optiondef CHECK_EQ(join(error_rules(d)), ""); d.mission().options = {"SLD_MkOne", "SLD_MkTwo"}; CHECK_EQ(join(error_rules(d)), "D1"); d.mission().options = {"ind_maglat"}; // spelled differently: still a member CHECK_EQ(join(error_rules(d)), ""); } TEST(D2_applied_techs_order) { const Catalog& c = mini_catalog(); const ShipSectionDef& eng = *c.section("Human", "DEFission"); CHECK_EQ(join(applied_techs(eng, {"IND_RefCoat", "DRV_RecFiss", "IND_PlyAlloy"})), "DRV_Fissn,DRV_Node,IND_PlyAlloy,IND_RefCoat,DRV_RecFiss"); CHECK_EQ(join(applied_techs(eng, {})), "DRV_Fissn,DRV_Node"); const ShipSectionDef& cr = *c.section("Human", "CRFission"); CHECK_EQ(join(applied_techs(cr, {})), "IND_CruisCon,DRV_Fissn,DRV_Node"); const ShipSectionDef& st = *c.section("Human", "DNStation"); CHECK_EQ(join(applied_techs(st, {})), "IND_OrbCom"); // station: no hull-class tech const ShipSectionDef& arm = *c.section("Human", "DEArmor"); CHECK_EQ(join(applied_techs(arm, {"SLD_MkTwo", "IND_QrkRes"})), "IND_QrkRes,SLD_MkTwo"); // optiondef last CHECK_EQ(join(applied_techs(arm, {"NotAnOption"})), ""); // non-members are not applied CHECK_EQ(join(applied_techs(arm, {"ind_qrkres"})), "ind_qrkres"); // spelled as chosen } TEST(D2_options_from_applied_round_trip) { const Catalog& c = mini_catalog(); const ShipSectionDef& cr = *c.section("Human", "CRFission"); std::vector dopts = applied_techs(cr, {"IND_PlyAlloy"}); CHECK_EQ(join(dopts), "IND_CruisCon,DRV_Fissn,DRV_Node"); // CRFission has no option groups in the mini root CHECK_EQ(join(options_from_applied(cr, dopts)), ""); const ShipSectionDef& eng = *c.section("Human", "DEFission"); dopts = applied_techs(eng, {"IND_MagLat", "DRV_RecFiss"}); CHECK_EQ(join(options_from_applied(eng, dopts)), "IND_MagLat,DRV_RecFiss"); // strip on a whole design record (the save's DOpts include the structural techs) Design d = armor(); d.engine().options = {"DRV_Fissn", "DRV_Node", "IND_RefCoat"}; d.command().options = {"IND_PlyAlloy"}; mini_rules().strip_applied_techs(d); CHECK_EQ(join(d.engine().options), "IND_RefCoat"); CHECK_EQ(join(d.command().options), "IND_PlyAlloy"); }