sots-engine/tests/game_design/realdata_test.cpp

170 lines
7.1 KiB
C++

// Every design in the owner's real saves through the rules.
//
// SOTS_DATA_DIR=<extracted gob> SOTS_DESIGNS_JSON=<stock_designs.json> realdata_test
//
// Skips (exit 0) when either variable is unset. Checks:
// * the (species, section id) record resolves to the section the reference
// reader named, and the save's bank count is the section's bank count;
// * stripping the structural techs from DOpts yields the reference's options
// and applied_techs() rebuilds DOpts exactly (D2) on every instance;
// * no design has an error-level finding (structure, banks, fit, options,
// tech gating against the owning player's researched techs);
// * the only tech-gating findings are warn-level, on hidden designs (the
// per-race default rider design the engine creates without gating).
// With the owner's 127-design extract the counts must be 127/127, 197/197 and
// 121/127 with the 6 hidden Tarkas assault shuttles as the warnings.
#include <cstdio>
#include <cstdlib>
#include <filesystem>
#include <string>
#include <vector>
#include "game/data/catalog.h"
#include "game/design/rules.h"
#include "game/design/stats.h"
#include "stock_designs.h"
using namespace game::data;
using namespace game::design;
namespace {
std::string join(const std::vector<std::string>& v) {
std::string s;
for (const std::string& x : v) s += (s.empty() ? "" : ",") + x;
return s;
}
bool same_fold(const std::vector<std::string>& a, const std::vector<std::string>& b) {
if (a.size() != b.size()) return false;
for (std::size_t i = 0; i < a.size(); ++i)
if (!iequals(a[i], b[i])) return false;
return true;
}
bool is_gating(const std::string& rule) { return rule.size() == 2 && rule[0] == 'C'; }
} // namespace
int main() {
const char* data_dir = std::getenv("SOTS_DATA_DIR");
const char* designs_path = std::getenv("SOTS_DESIGNS_JSON");
if (!data_dir || !*data_dir || !designs_path || !*designs_path) {
std::printf("SKIP: SOTS_DATA_DIR / SOTS_DESIGNS_JSON not set\n");
return 0;
}
Catalog cat = load_catalog(data_dir);
if (cat.sections.empty() || cat.weapons.empty()) {
std::printf("FAIL: no catalog at %s (%zu problems)\n", data_dir, cat.problems.size());
return 1;
}
std::vector<StockDesign> designs;
std::string err;
if (!load_stock_designs(designs_path, designs, err)) {
std::printf("FAIL: %s\n", err.c_str());
return 1;
}
Ruleset rules(cat);
int failures = 0;
int n_designs = 0, n_clean = 0, n_instances = 0, n_dopts_ok = 0, n_gated_ok = 0, n_gate_warn_hidden = 0;
std::vector<std::string> gate_warn_designs;
for (StockDesign& sd : designs) {
++n_designs;
Design& d = sd.design;
const std::string tag = sd.save + " " + sd.player + " '" + d.name + "'";
// resolution against the reference view
for (std::size_t i = 0; i < 3; ++i) {
const SectionUse& u = d.slot(kUsedSlots[i]);
const ShipSectionDef* s = u.empty() ? nullptr : rules.resolve_section(u.species, u);
const std::string got = s ? s->stem : "";
if (!iequals(got, sd.expect_stem[i])) {
std::printf("FAIL %s: %s slot resolves to '%s', reference says '%s'\n", tag.c_str(),
std::string(slot_name(kUsedSlots[i])).c_str(), got.c_str(), sd.expect_stem[i].c_str());
++failures;
}
if (s && static_cast<int>(s->banks.size()) != sd.bank_count[i]) {
std::printf("FAIL %s: %s has %zu banks, save stores %d\n", tag.c_str(), s->stem.c_str(), s->banks.size(),
sd.bank_count[i]);
++failures;
}
}
// DOpts -> chosen options -> DOpts (D2)
rules.strip_applied_techs(d);
for (std::size_t i = 0; i < 3; ++i) {
const SectionUse& u = d.slot(kUsedSlots[i]);
if (u.empty()) continue;
const ShipSectionDef* s = rules.resolve_section(u.species, u);
if (!s) continue;
++n_instances;
if (!same_fold(u.options, sd.expect_options[i])) {
std::printf("FAIL %s: %s options [%s], reference [%s]\n", tag.c_str(), s->stem.c_str(), join(u.options).c_str(),
join(sd.expect_options[i]).c_str());
++failures;
}
std::vector<std::string> applied = applied_techs(*s, u.options);
if (same_fold(applied, sd.dopts[i])) {
++n_dopts_ok;
} else {
std::printf("FAIL %s: %s applied_techs [%s], DOpts [%s]\n", tag.c_str(), s->stem.c_str(), join(applied).c_str(),
join(sd.dopts[i]).c_str());
++failures;
}
}
// the rules
std::vector<Violation> v = rules.validate(d);
bool errors = false, gate = false, gate_error = false;
for (const Violation& x : v) {
if (x.level == Level::Error) errors = true;
if (is_gating(x.rule)) {
gate = true;
if (x.level != Level::Warn) gate_error = true;
}
}
if (errors) {
std::printf("FAIL %s:\n", tag.c_str());
for (const Violation& x : v) std::printf(" %s\n", x.str().c_str());
++failures;
} else {
++n_clean;
}
if (!gate) {
++n_gated_ok;
} else if (!gate_error && d.hidden) {
++n_gate_warn_hidden;
gate_warn_designs.push_back(sd.race_text + " '" + d.name + "' (" + sd.save + ", " + sd.player + ")");
} else {
std::printf("FAIL %s: tech gating finding that is not a warn on a hidden design\n", tag.c_str());
for (const Violation& x : v) std::printf(" %s\n", x.str().c_str());
++failures;
}
// derive_stats must run on everything
DesignStats st = derive_stats(rules, d);
if (st.sections.empty()) {
std::printf("FAIL %s: no stats\n", tag.c_str());
++failures;
}
}
std::printf("designs: %d, clean (structure/banks/fit/options/gating): %d/%d\n", n_designs, n_clean, n_designs);
std::printf("section instances: %d, DOpts reproduced by applied_techs: %d/%d\n", n_instances, n_dopts_ok, n_instances);
std::printf("tech gating: %d/%d pass, %d warn-only on hidden designs:\n", n_gated_ok, n_designs, n_gate_warn_hidden);
for (const std::string& s : gate_warn_designs) std::printf(" %s\n", s.c_str());
if (n_designs == 127) { // the owner's three-save extract: the published numbers
if (n_clean != 127 || n_instances != 197 || n_dopts_ok != 197 || n_gated_ok != 121 || n_gate_warn_hidden != 6) {
std::printf("FAIL: expected 127/127 clean, 197/197 DOpts, 121/127 gated + 6 hidden warnings\n");
++failures;
}
for (const std::string& s : gate_warn_designs)
if (s.rfind("Tarkas 'Default Assault Shuttle'", 0) != 0) {
std::printf("FAIL: unexpected gating warning on %s\n", s.c_str());
++failures;
}
}
std::printf("%s\n", failures ? "FAILED" : "OK");
return failures ? 1 : 0;
}