#!/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 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))