170 lines
7.1 KiB
C++
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;
|
|
}
|