champions-of-luneraria/tools/port/trace_diff.py
Martin Stickelbroeck 44d3bcdee9 U1.5 Unity Core Integration (1/2): Unity-Projekt, Test Runner, Diagnose
- Projekt mit Unity 6.3 LTS (6000.3.20f1, Personal) geöffnet: ProjectSettings,
  .meta-Dateien, URP-Global-Settings, packages-lock.json.
- Manifest auf die benötigten Pakete zurückgeführt; Unity hatte beim ersten
  Öffnen Vorlagenpakete ergänzt (Purchasing, Analytics, Collab, XR, 2D, Navigation).
- Test Runner: Die U1-Suiten laufen unverändert als EditMode-Tests; mit
  LUNE_TEST_REPORT schreiben .NET und Unity dasselbe JSON-Protokoll (SuiteReport).
- Golden-Vergleich zusätzlich je Fachbereich (Gold, HP, Positionen, Türme,
  Ancients, Wellen, Helden, Boss, Gewinner, ...). Nur Bericht; Testwerte und
  Prüfungsanzahl unverändert.
- Diagnose: FloatProbe (Kern-Arithmetik gegen Godot-Bits und Auswertungsart der
  Laufzeit), DivergenceTrace und tools/port/trace_diff.py (erster abweichender
  Schritt), CoreBenchmark für .NET und Editor, CoreBatch als -executeMethod-Einstieg.
- .NET-Harness baut nach tools/out/, weil Unity Temp/ beim Beenden löscht.
- .gitignore ignoriert nur noch die csproj/sln im Projektwurzelverzeichnis;
  die Projektdateien des .NET-Harness fehlten deshalb in U1 und kommen jetzt dazu.

Stand dieses Commits im Unity-Editor (Mono): 4 von 5 Suiten grün; golden
152/157, weil Mono float32-Zwischenwerte doppelt genau rechnet (Teil 2/2).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:19:08 +02:00

77 lines
3.1 KiB
Python

#!/usr/bin/env python3
"""Compares two step traces of DivergenceTrace (docs/port/U1.5-UNITY.md).
Reports the first step where raw bits differ (positions, hit points) and the first step
where a decision differs (alive, target, hits, zone, combat end), with the units involved.
Usage: python3 tools/port/trace_diff.py <reference trace> <other trace>
"""
import struct
import sys
def load(path):
steps = []
current = None
for line in open(path, encoding="utf-8"):
line = line.rstrip("\n")
if line.startswith("step "):
parts = line.split()
current = {"step": int(parts[1]), "t": float(parts[3]), "finished": parts[5], "units": {}}
steps.append(current)
elif line.startswith(" u ") and current is not None:
discrete, bits = line[4:].split(" | ")
fields = discrete.split()
b = bits.split()
current["units"][int(fields[0])] = {
"discrete": " ".join(fields[1:]),
"hp": int(b[1], 16),
"x": int(b[3], 16),
"y": int(b[4], 16),
}
return steps
def f32(bits):
return struct.unpack("<f", struct.pack("<i", bits if bits < 2**31 else bits - 2**32))[0]
def f64(bits):
return struct.unpack("<d", struct.pack("<Q", bits))[0]
def main():
ref, other = load(sys.argv[1]), load(sys.argv[2])
print(f"steps: reference {len(ref) - 1}, other {len(other) - 1}")
first_bits = first_decision = None
worst_pos = 0.0
worst_hp = 0.0
for a, b in zip(ref, other):
for uid in sorted(set(a["units"]) | set(b["units"])):
ua, ub = a["units"].get(uid), b["units"].get(uid)
if ua is None or ub is None:
if first_decision is None:
first_decision = (a["step"], uid, ua and ua["discrete"], ub and ub["discrete"])
continue
if (ua["x"], ua["y"], ua["hp"]) != (ub["x"], ub["y"], ub["hp"]):
if first_bits is None:
first_bits = (a["step"], uid)
if first_decision is None:
worst_pos = max(worst_pos, abs(f32(ua["x"]) - f32(ub["x"])), abs(f32(ua["y"]) - f32(ub["y"])))
worst_hp = max(worst_hp, abs(f64(ua["hp"]) - f64(ub["hp"])))
if ua["discrete"] != ub["discrete"] and first_decision is None:
first_decision = (a["step"], uid, ua["discrete"], ub["discrete"])
if a["finished"] != b["finished"] and first_decision is None:
first_decision = (a["step"], "combat end", a["finished"], b["finished"])
print("first bit difference: step %s unit %s" % first_bits if first_bits else "no bit difference")
if first_decision:
step, uid, da, db = first_decision
print(f"first decision difference: step {step} (t = {step * 0.05:.2f} s) unit {uid}")
print(f" reference: {da}")
print(f" other: {db}")
print(f"largest deviation before it: position {worst_pos:.3e} m, hp {worst_hp:.3e}")
else:
print("no decision difference")
if __name__ == "__main__":
main()