#!/usr/bin/env python3 """compare.py Runs the reference validator (design_rules.py from the RE repo) on the same designs the C++ dump was produced from and diffs, design by design: * the set of findings as (rule, level, slot, bank) -- must be identical; * applied_techs per slot -- must be identical (case-insensitive); * the chosen options per slot -- must be identical; * derived stats: mass, costs, health, crew, cpoints, banks, turrets, command/maintenance cost, command quota, drive figures, capacities, and per-section/per-bank figures -- numbers numerically, bools as 0/1. The reference loads its catalogs from SOTS_CATALOG_DIR (default: the RE repo's verify/results/data-catalogs). Exit 1 on any difference. """ import json import os import sys MAX_DIFFS = 40 def norm_val(v): if v is None or v == "": return None # an absent string field is "" on our side, None on the reference's if isinstance(v, bool): return 1 if v else 0 if isinstance(v, (int, float)): return float(v) if isinstance(v, str): s = v.strip() if s.lower() in ("true", "false"): return 1 if s.lower() == "true" else 0 try: return float(s) except ValueError: return s.lower() return v def same(a, b): a, b = norm_val(a), norm_val(b) if isinstance(a, float) and isinstance(b, float): return abs(a - b) <= 1e-9 * max(1.0, abs(a), abs(b)) return a == b def main(argv): if len(argv) != 4: print(__doc__) return 2 ours_path, designs_path, rules_dir = argv[1:] sys.path.insert(0, rules_dir) import design_rules # noqa: E402 cat = design_rules.Catalog.load(os.environ.get("SOTS_CATALOG_DIR")) ours = json.load(open(ours_path)) players = json.load(open(designs_path)) ref = [(pi, di, p, d) for pi, p in enumerate(players) for di, d in enumerate(p["designs"])] if len(ref) != len(ours): print(f"design count differs: ours {len(ours)}, reference {len(ref)}") return 1 diffs = [] n_viol_ours = n_viol_ref = 0 n_designs_with_findings = 0 n_stats = 0 def diff(tag, what, a, b): diffs.append(f"{tag}: {what}: ours {a!r} vs reference {b!r}") for o, (pi, di, p, d) in zip(ours, ref): tag = f"{p['save']} {p['player']} '{d['name']}'" if (o["player_index"], o["design_index"]) != (pi, di): diff(tag, "order", (o["player_index"], o["design_index"]), (pi, di)) continue # findings ref_v = design_rules.validate(d, cat) ref_set = {(v.rule, v.level, v.slot, v.bank) for v in ref_v} our_set = {(v["rule"], v["level"], v["slot"], v["bank"]) for v in o["violations"]} n_viol_ours += len(our_set) n_viol_ref += len(ref_set) if ref_set: n_designs_with_findings += 1 if ref_set != our_set: diff(tag, "findings", sorted(our_set - ref_set, key=str), sorted(ref_set - our_set, key=str)) # applied techs / options / stats st = design_rules.derive_stats(d, cat) for slot in design_rules.SLOTS: use = d.get(slot) if not use: if slot in o["applied_techs"]: diff(tag, f"{slot} applied_techs", o["applied_techs"][slot], None) continue want = [t.lower() for t in st["applied_techs"].get(slot, [])] got = [t.lower() for t in o["applied_techs"].get(slot, [])] if want != got: diff(tag, f"{slot} applied_techs", got, want) wopt = [t.lower() for t in use.get("options", [])] gopt = [t.lower() for t in o["options"].get(slot, [])] if wopt != gopt: diff(tag, f"{slot} options", gopt, wopt) os_ = o["stats"] for key in ("mass", "section_cost", "section_cost_with_options", "weapon_cost_per_bank", "weapon_cost_per_mount", "total_cost_estimate", "health_total", "crew", "cpoints", "banks", "turrets", "command_cost", "maintenance_cost", "command_quota", "ftlspeed", "nodespeed", "engine_techera", "hull_class"): n_stats += 1 a, b = norm_val(os_.get(key)), norm_val(st.get(key)) if a is None and b is None: continue if (a is None) != (b is None) or not same(a, b): diff(tag, key, a, b) for key, val in st["capacities"].items(): n_stats += 1 if key not in os_["capacities"]: diff(tag, f"capacities.{key}", None, val) elif not same(os_["capacities"][key], val): diff(tag, f"capacities.{key}", os_["capacities"][key], val) for key in os_["capacities"]: if key not in st["capacities"]: diff(tag, f"capacities.{key}", os_["capacities"][key], None) for slot, sec in st["sections"].items(): osec = os_["sections"].get(slot) if osec is None: diff(tag, f"{slot} section stats", None, sec["section"]) continue for key in ("section", "mass", "cost", "option_cost_multiplier", "health", "crew", "cpoints"): n_stats += 1 if not same(osec[key], sec[key]): diff(tag, f"{slot}.{key}", osec[key], sec[key]) if len(osec["banks"]) != len(sec["banks"]): diff(tag, f"{slot} bank count", len(osec["banks"]), len(sec["banks"])) continue for i, (ob, rb) in enumerate(zip(osec["banks"], sec["banks"])): for key in ("size", "class", "mounts", "weapon", "weapon_cost", "turret_model", "turret_health"): n_stats += 1 a, b = norm_val(ob.get(key)), norm_val(rb.get(key)) if a is None and b is None: continue if (a is None) != (b is None) or not same(a, b): diff(tag, f"{slot} bank {i} {key}", a, b) print(f"{len(ours)} designs compared; findings ours {n_viol_ours} / reference {n_viol_ref} " f"({n_designs_with_findings} designs with findings); {n_stats} stat values compared") if diffs: print(f"{len(diffs)} differences:") for line in diffs[:MAX_DIFFS]: print(" " + line) if len(diffs) > MAX_DIFFS: print(f" ... {len(diffs) - MAX_DIFFS} more") return 1 print("no differences") return 0 if __name__ == "__main__": sys.exit(main(sys.argv))