158 lines
6.5 KiB
Python
158 lines
6.5 KiB
Python
#!/usr/bin/env python3
|
|
"""compare.py <ours.json> <stock_designs.json> <design-rules-dir>
|
|
|
|
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))
|