diff options
Diffstat (limited to 'scripts/analyze-p43-hud.py')
| -rw-r--r-- | scripts/analyze-p43-hud.py | 130 |
1 files changed, 130 insertions, 0 deletions
diff --git a/scripts/analyze-p43-hud.py b/scripts/analyze-p43-hud.py new file mode 100644 index 0000000..ad0a859 --- /dev/null +++ b/scripts/analyze-p43-hud.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +"""P43 HUD analysis: prove the new calendar card + energy bar + hotbar are real. + +Reads the `LV_P43_SHOT=1` frames and the shot log (which prints the layout rects +computed by HudLayout in the running game), then writes +docs/evidence/p43-analysis.txt with the measured numbers: + + 1. calendar card / hotbar / slot0 rectangles exactly as the game laid them out, + all inside the 1280x720 viewport and non-overlapping; + 2. per-slot hotbar brightness (10 distinct cells) and the selected-slot delta; + 3. calendar-card and hotbar opacity is semi-transparent (never an opaque block). + +Usage: + python3 scripts/analyze-p43-hud.py <record-dir> <shot-log> <out-txt> + +Exit 0 when checks pass, 1 when they fail, 2 on usage/read error. +""" + +import re +import sys +from pathlib import Path + +import numpy as np +from PIL import Image + +WIDTH = 1280 +HEIGHT = 720 +SHOT_RE = re.compile( + r"p43-shot=(\S+) tick=(\d+) calendar=([\d,]+) hotbar=([\d,]+) slot0=([\d,]+) " + r"selected=(\d+) energy=([\d.]+) day=(\d+) season=(\S+) period=(\S+) clock=(\S+)" +) + + +def rect(text): + x, y, w, h = (int(v) for v in text.split(",")) + return (x, y, w, h) + + +def inside(name, r): + x, y, w, h = r + return x >= 0 and y >= 0 and x + w <= WIDTH and y + h <= HEIGHT + + +def main(argv): + if len(argv) != 4: + sys.stderr.write(__doc__) + return 2 + record_dir = Path(argv[1]) + log_text = Path(argv[2]).read_text(encoding="utf-8") + shots = [] + for line in log_text.splitlines(): + m = SHOT_RE.search(line) + if m: + shots.append( + { + "name": m.group(1), + "tick": int(m.group(2)), + "calendar": rect(m.group(3)), + "hotbar": rect(m.group(4)), + "slot0": rect(m.group(5)), + "selected": int(m.group(6)), + "energy": float(m.group(7)), + "day": int(m.group(8)), + "season": m.group(9), + "period": m.group(10), + "clock": m.group(11), + } + ) + if not shots: + sys.stderr.write("no p43-shot lines in log\n") + return 1 + + lines = ["P43 HUD analysis (LV_P43_SHOT=1)", ""] + failures = [] + for shot in shots: + img = np.asarray(Image.open(record_dir / f"{shot['name']}.png").convert("RGBA"), dtype=np.float64) + lines.append(f"[{shot['name']}] tick={shot['tick']} day={shot['day']} season={shot['season']} " + f"period={shot['period']} clock={shot['clock']} energy={shot['energy']:.0f} selected={shot['selected']}") + cx, cy, cw, ch = shot["calendar"] + bx, by, bw, bh = shot["hotbar"] + sx, sy, sw, sh = shot["slot0"] + lines.append(f" calendar_rect = ({cx},{cy},{cw}x{ch}) hotbar_rect = ({bx},{by},{bw}x{bh}) slot0_rect = ({sx},{sy},{sw}x{sh})") + for name in ("calendar", "hotbar", "slot0"): + if not inside(name, shot[name]): + failures.append(f"{shot['name']} {name} out of viewport {shot[name]}") + # calendar must not overlap the top strip (y >= 39) and must sit left of hotbar band. + if cy < 39: + failures.append(f"{shot['name']} calendar overlaps top bar") + + # 1) per-slot brightness across the 10 slots (distinct cells, selected delta). + slot_means = [] + for index in range(10): + x = bx + 8 + index * (sw + 6) + y = by + 8 + cell = img[y:y + sh, x:x + sw, :3].mean() + slot_means.append(cell) + sel = shot["selected"] + others = [v for i, v in enumerate(slot_means) if i != sel] + lines.append(" hotbar_slot_mean_luma = " + ", ".join(f"{v:.1f}" for v in slot_means)) + lines.append(f" selected_slot={sel} selected_luma={slot_means[sel]:.1f} others_mean={np.mean(others):.1f} " + f"delta={slot_means[sel] - np.mean(others):+.1f}") + + # 2) opacity: the calendar card and hotbar backdrops must be semi-transparent. + for name, r in (("calendar", shot["calendar"]), ("hotbar", shot["hotbar"])): + x, y, w, h = r + alpha = img[y:y + h, x:x + w, 3] + region = img[y:y + h, x:x + w, :3] + opaque_white = int(((alpha >= 250) & (region.min(axis=2) >= 250)).sum()) + lines.append(f" {name}: alpha_min={int(alpha.min())} alpha_max={int(alpha.max())} " + f"mean_luma={region.mean():.1f} opaque_white_px={opaque_white}") + if opaque_white > 0: + failures.append(f"{shot['name']} {name} contains opaque white pixels") + if region.mean() < 8.0: + failures.append(f"{shot['name']} {name} too dark (possible big black block)") + lines.append("") + + lines.append("verdict = " + ("PASS" if not failures else "FAIL")) + out_txt = Path(argv[3]) + out_txt.parent.mkdir(parents=True, exist_ok=True) + out_txt.write_text("\n".join(lines) + "\n", encoding="utf-8") + sys.stdout.write("\n".join(lines) + "\n") + if failures: + for f in failures: + sys.stderr.write(f"FAIL {f}\n") + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv)) |
