sots-engine/tests/game_design/test_tech.cpp

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