sots-engine/tests/game_design/oracle/compare.py

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))