summaryrefslogtreecommitdiff
path: root/scripts/analyze-p43-hud.py
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/analyze-p43-hud.py')
-rw-r--r--scripts/analyze-p43-hud.py130
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))