// Every design in the owner's real saves through the rules. // // SOTS_DATA_DIR= SOTS_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 #include #include #include #include #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& v) { std::string s; for (const std::string& x : v) s += (s.empty() ? "" : ",") + x; return s; } bool same_fold(const std::vector& a, const std::vector& 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 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 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(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 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 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; }