172 lines
6.6 KiB
C++
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.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());
|
|
}
|