sots-engine/tests/game_data/test_techtree.cpp

172 lines
6.6 KiB
C++

// TechTree: allows edges, groups, links, on a hand-written tree.
#include "game/data/techtree.h"
#include "test_main.h"
using namespace game::data;
namespace {
const char* kTree =
"tech\n{\n"
"\tname \t\"IND_ROOT\"\n"
"\tfamily\t\"IND\"\n"
"\tallows \t\"IND_Waldo \t\tRP:5000\"\n"
"\tallows \t\"IND_StlthArm \tRP:0\t\tHuman:0 Zuul:0 Hiver:0 Tarkas:0 Liir:0 Morrigi:100\"\n"
"}\n"
"tech {\n"
"\tname\t\"IND_Waldo\"\n"
"\tthreat\t2\n"
"\ttype\tP\n"
"\tallows\t\"IND_PlyAlloy RP:20000 Human:50 Liir:95\"\n"
"\tallows\t\"WEP_Dsrptr RP:10000 Human:20 Zuul:90 Hiver:40 Tarkas:80 Liir:90 Morrigi:80\"\n"
"\tstrategy { inc TECHBEN_INDOUTPUT inc TECHBEN_HULLSTR dec TECHBEN_SHIPCONCOST }\n"
"\tship { section DEHammerhead section CRHammerhead }\n"
"}\n"
"tech {\n"
"\tname\t\"IND_PlyAlloy\"\n"
"\tOPTION_COST\t1.2\n"
"\trequires\t\"IND_Waldo\"\n"
"}\n"
"tech {\n"
"\tname\t\"IND_StlthArm\"\n"
"\tunlock_explicitly\ttrue\n"
"}\n"
"tech {\n"
"\tname\t\"WEP_Dsrptr\"\n"
"\tfamily\t\"TRP\"\n"
"\tGROUP\tTorps\n"
"\tweapon { filename \"Weapons/trp_disruptor.weapon\" }\n"
"}\n"
"tech {\n"
"\tname\t\"WEP_PhotTrp\"\n"
"\tgroup\tTORPS\n"
"\trequires\tGRP_Torps\n"
"\trequires\tGRP_Mines\n"
"}\n";
} // namespace
TEST(allows_parsing_full_and_partial) {
AllowsEdge e = parse_allows("DRV_Node \t\tRP:0\t\tHuman:100\tZuul:0\t\tHiver:0\t\tTarkas:0\tLiir:0\t\tMorrigi:0", "DRV_ROOT", 7);
CHECK_EQ(e.from, "DRV_ROOT");
CHECK_EQ(e.to, "DRV_Node");
CHECK(e.rp && *e.rp == 0);
CHECK_EQ(e.percent(Species::Human), 100);
CHECK_EQ(e.percent(Species::Zuul), 0);
CHECK_EQ(e.percent(Species::Morrigi), 0);
CHECK_EQ(e.percent(Species::NPC), 100); // never written; engine default
CHECK(e.pct_written[static_cast<int>(Species::Human)]);
CHECK(!e.pct_written[static_cast<int>(Species::NPC)]);
CHECK(e.unparsed.empty());
CHECK_EQ(e.line, 7);
AllowsEdge p = parse_allows("DRV_Fusn RP:85000", "DRV_Fissn");
CHECK(p.rp && *p.rp == 85000);
for (int i = 0; i < kSpeciesCount; ++i) {
CHECK_EQ(p.pct[static_cast<std::size_t>(i)], kDefaultAllowPercent);
CHECK(!p.pct_written[static_cast<std::size_t>(i)]);
}
AllowsEdge q = parse_allows("X rp:5 human:10", "Y"); // key case does not matter
CHECK(q.rp && *q.rp == 5);
CHECK_EQ(q.percent(Species::Human), 10);
}
TEST(allows_unparsed_tokens_are_reported) {
std::vector<Problem> problems;
AllowsEdge e = parse_allows("CHILD Klingon:50 bogus RP:x", "T", 3, &problems, "f.tech");
CHECK_EQ(e.to, "CHILD");
CHECK(!e.rp);
CHECK_EQ(e.unparsed.size(), std::size_t{3});
CHECK_EQ(problems.size(), std::size_t{4}); // no RP + 3 tokens
CHECK(problems[0].kind == Problem::Kind::Unparsed);
CHECK_EQ(problems[0].file, "f.tech");
std::vector<Problem> none;
AllowsEdge empty = parse_allows(" ", "T", 1, &none);
CHECK_EQ(empty.to, "");
CHECK_EQ(none.size(), std::size_t{1});
}
TEST(tech_tree_nodes_edges_groups) {
auto r = parse_tech_tree(kTree, "MasterTechList.tech");
CHECK(r.ok());
CHECK(r.problems.empty());
const TechTree& t = *r.value;
CHECK_EQ(t.nodes.size(), std::size_t{6});
CHECK_EQ(t.edges.size(), std::size_t{4});
const TechNode* root = t.find("ind_root"); // case-insensitive
CHECK(root != nullptr);
CHECK_EQ(root->family, "IND");
CHECK_EQ(root->family_inferred, "IND");
CHECK(root->is_root());
CHECK_EQ(root->allows.size(), std::size_t{2});
const AllowsEdge& stealth = t.edges[root->allows[1]];
CHECK_EQ(stealth.to, "IND_StlthArm");
CHECK_EQ(stealth.percent(Species::Morrigi), 100);
CHECK_EQ(stealth.percent(Species::Human), 0);
CHECK_EQ(stealth.percent(Species::NPC), 100);
const TechNode* waldo = t.find("IND_Waldo");
CHECK(waldo != nullptr);
CHECK(waldo->threat && *waldo->threat == 2);
CHECK_EQ(waldo->type, "P");
CHECK_EQ(waldo->family, "");
CHECK_EQ(waldo->family_inferred, "IND");
CHECK(!waldo->is_root());
CHECK_EQ(waldo->benefits_inc.size(), std::size_t{2});
CHECK_EQ(waldo->benefits_inc[1], "TECHBEN_HULLSTR");
CHECK_EQ(waldo->benefits_dec.size(), std::size_t{1});
CHECK_EQ(waldo->sections.size(), std::size_t{2});
CHECK_EQ(waldo->sections[0], "DEHammerhead");
const AllowsEdge& ply = t.edges[waldo->allows[0]];
CHECK_EQ(ply.percent(Species::Human), 50);
CHECK_EQ(ply.percent(Species::Liir), 95);
CHECK_EQ(ply.percent(Species::Zuul), 100); // unlisted -> default
const TechNode* alloy = t.find("IND_PlyAlloy");
CHECK(alloy->option_cost && *alloy->option_cost == 1.2); // OPTION_COST key case
CHECK_EQ(alloy->requires_tech.size(), std::size_t{1});
CHECK(t.find("IND_StlthArm")->unlock_explicitly.value_or(false));
const TechNode* dsr = t.find("WEP_Dsrptr");
CHECK_EQ(dsr->family, "TRP");
CHECK_EQ(dsr->family_inferred, "WEP");
CHECK_EQ(dsr->group, "Torps");
CHECK_EQ(dsr->weapon_files.size(), std::size_t{1});
CHECK_EQ(dsr->weapon_files[0], "Weapons/trp_disruptor.weapon");
CHECK_EQ(t.groups.size(), std::size_t{1});
CHECK_EQ(t.groups[0].first, "TORPS");
CHECK_EQ(t.groups[0].second.size(), std::size_t{2});
const auto* m = t.group_members("GRP_torps");
CHECK(m && m->size() == 2 && (*m)[1] == "WEP_PhotTrp");
CHECK(t.group_members("TORPS") == m);
CHECK(t.group_members("GRP_MINES") == nullptr);
CHECK(t.requirement_exists("GRP_Torps"));
CHECK(!t.requirement_exists("GRP_Mines"));
CHECK(t.requirement_exists("ind_waldo"));
CHECK(!t.requirement_exists("IND_Nope"));
CHECK_EQ(t.edges_to("WEP_Dsrptr").size(), std::size_t{1});
CHECK_EQ(t.edges_from("IND_ROOT").size(), std::size_t{2});
auto roots = t.roots();
CHECK_EQ(roots.size(), std::size_t{2}); // IND_ROOT and WEP_PhotTrp; everything else is allowed by something
CHECK_EQ(roots[0]->name, "IND_ROOT");
CHECK_EQ(roots[1]->name, "WEP_PhotTrp");
}
TEST(tech_tree_problems) {
auto r = parse_tech_tree("tech { name \"A\" allows \"B\" } tech { name \"a\" } tech { threat 1 }", "t.tech");
CHECK(r.ok());
int unparsed = 0, dup = 0, missing = 0;
for (const Problem& p : r.problems) {
if (p.kind == Problem::Kind::Unparsed) ++unparsed;
if (p.kind == Problem::Kind::Duplicate) ++dup;
if (p.kind == Problem::Kind::MissingKey) ++missing;
}
CHECK_EQ(unparsed, 1); // allows "B" without RP:
CHECK_EQ(dup, 1); // "a" repeats "A" case-insensitively
CHECK_EQ(missing, 1); // nameless tech dropped
CHECK_EQ(r.value->nodes.size(), std::size_t{2});
CHECK(!parse_tech_tree("weapon { }").ok());
}