190 lines
8.8 KiB
C++
190 lines
8.8 KiB
C++
// 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<std::string> 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<std::string>{"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<std::string>{"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_tech DRV_Fissn (known) and XXX_Nowhere (not in the tree):
|
|
// a token the tree cannot satisfy is reported, never an error.
|
|
std::vector<Violation> 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<std::string>{"DRV_Fissn", "DRV_Node", "WEP_GsDrvr"};
|
|
std::vector<Violation> 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<std::string>{"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<std::string>{"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<std::string>{"DRV_Fissn"};
|
|
std::vector<Violation> 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<std::string>{};
|
|
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<std::string>{"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<std::string> 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");
|
|
}
|