#!/usr/bin/env python3 """P77(日历卡/浮动提示药丸化 + 面板与 hotbar 不遮挡)证据分析。 判据(读真实实跑日志 + 区域像素统计;近黑阈值 24 对齐旧纯黑底色): 1. 日志含 p43-shot day/night(日历 rect + hotbar rect)、p44-shot 对话 rect、p67 任务面板。 2. 日历卡、交互面板、提示带:近黑占比 < 20%、无实心 24x24 近黑块、亮文字像素充足。 3. 几何不遮挡:面板/对话弹层底边 <= hotbar 顶(p67 面板用同一 panelRect 公式复核)。 4. 昼/夜、对话两态帧 pairwise diff > 0。 5. 观感由 Hermes vision 侧复核,非本脚本判据。 Usage: python3 scripts/analyze-p77.py Exit 0 pass / 1 fail / 2 usage. """ import re import sys from pathlib import Path import numpy as np from PIL import Image from numpy.lib.stride_tricks import sliding_window_view P43_RE = re.compile( r"^p43-shot=(p43-hud-(?:day|night))\S* tick=(\d+) calendar=(\d+),(\d+),(\d+),(\d+) hotbar=(\d+),(\d+),(\d+),(\d+)", re.MULTILINE, ) P44_RE = re.compile(r"^p44-shot=(p44-dialog-[a-z]) panel=dialogue rect=(\d+),(\d+),(\d+),(\d+)", re.MULTILINE) THR = 24 BLOCK = 24 DARK_FRAC_MAX = 0.20 BAR_HEIGHT = 36 PANEL_LINE_HEIGHT = 18 PANEL_MAX_WIDTH = 380 PANEL_TOP_EXTRA = 26 def region_stats(path, rect): x, y, w, h = rect rgb = np.asarray(Image.open(path).convert("RGB"), dtype=np.int16) r = rgb[y : y + h, x : x + w] mask = (r[:, :, 0] <= THR) & (r[:, :, 1] <= THR) & (r[:, :, 2] <= THR) dark_frac = float(mask.mean()) if mask.size else 1.0 block = 0 if mask.shape[0] >= BLOCK and mask.shape[1] >= BLOCK: block = int(sliding_window_view(mask.astype(np.int64), (BLOCK, BLOCK)).sum(axis=(2, 3)).max()) bright = int(((r[:, :, 0] >= 200) & (r[:, :, 1] >= 200) & (r[:, :, 2] >= 200)).sum()) return dark_frac, block, bright def panel_rect(viewport_w, viewport_h, line_count): width = min(PANEL_MAX_WIDTH, max(1, viewport_w - 32)) height = max(1, line_count) * PANEL_LINE_HEIGHT + PANEL_TOP_EXTRA # hotbarBarRect (mirrors HudLayout): 10*40 + 9*6 wide, bottom-anchored. hotbar_w = 10 * 40 + 9 * 6 hotbar_x = (viewport_w - hotbar_w) // 2 - 8 hotbar_y = viewport_h - 40 - 12 - 8 max_bottom = hotbar_y - 8 y = max(BAR_HEIGHT + 12, min(viewport_h - height - 16, max(0, max_bottom - height))) return (16, y, width, height), (hotbar_x, hotbar_y, hotbar_w, 40 + 16) def main(): if len(sys.argv) != 3: print(__doc__) return 2 rec = Path(sys.argv[1]) out = Path(sys.argv[2]) failures = [] lines = [ "P77(日历卡/浮动提示药丸化 + 面板与 hotbar 错开)证据分析 — 真实 Desktop xvfb 1280x720", "近黑阈值 24;实心块 = 24x24 全近黑。", "", ] p43 = (rec / "log-P43.txt").read_text(encoding="utf-8", errors="replace") if (rec / "log-P43.txt").exists() else "" frames = { m.group(1): { "tick": int(m.group(2)), "calendar": (int(m.group(3)), int(m.group(4)), int(m.group(5)), int(m.group(6))), "hotbar": (int(m.group(7)), int(m.group(8)), int(m.group(9)), int(m.group(10))), } for m in P43_RE.finditer(p43) } for name in ["p43-hud-day", "p43-hud-night"]: path = rec / f"{name}.png" if name not in frames or not path.exists(): failures.append(f"missing {name}") continue cal = frames[name]["calendar"] df, block, bright = region_stats(path, cal) lines.append(f"{name}: calendar={cal} dark_frac={df:.3f} maxblk={block} bright={bright}") if df >= DARK_FRAC_MAX: failures.append(f"{name} calendar dark_frac {df:.3f} >= {DARK_FRAC_MAX}") if block >= BLOCK * BLOCK: failures.append(f"{name} calendar has solid near-black block") if bright < 100: failures.append(f"{name} calendar bright {bright} < 100 (文字不可读?)") lines.append("") hotbar = frames.get("p43-hud-day", {}).get("hotbar") # 几何不遮挡:面板与对话弹层底边须 <= hotbar 顶。 prect, hbrect = panel_rect(1280, 720, 5) panel_bottom = prect[1] + prect[3] hb_top = hbrect[1] lines.append(f"panelRect(1280,720,5)={prect} bottom={panel_bottom} vs hotbar_top={hb_top}") if panel_bottom > hb_top: failures.append(f"panel bottom {panel_bottom} overlaps hotbar top {hb_top}") p67 = rec / "p67-target.png" if p67.exists(): df, block, bright = region_stats(p67, prect) lines.append(f"p67-target panel region={prect} dark_frac={df:.3f} maxblk={block} bright={bright}") if df >= DARK_FRAC_MAX: failures.append(f"task panel dark_frac {df:.3f} >= {DARK_FRAC_MAX}") if block >= BLOCK * BLOCK: failures.append("task panel has solid near-black block") if bright < 100: failures.append(f"task panel bright {bright} < 100") else: failures.append("missing p67-target.png") lines.append("") p44 = (rec / "log-P44.txt").read_text(encoding="utf-8", errors="replace") if (rec / "log-P44.txt").exists() else "" dialogs = {m.group(1): (int(m.group(2)), int(m.group(3)), int(m.group(4)), int(m.group(5))) for m in P44_RE.finditer(p44)} if "p44-dialog-a" in dialogs: drect = dialogs["p44-dialog-a"] d_bottom = drect[1] + drect[3] lines.append(f"p44-dialog-a rect={drect} bottom={d_bottom} vs hotbar_top={hb_top}") if hb_top is not None and d_bottom > hb_top: failures.append(f"dialog bottom {d_bottom} overlaps hotbar top {hb_top}") else: failures.append("missing p44-dialog-a rect") prompt_path = rec / "p44-dialog-c.png" if prompt_path.exists(): df, block, bright = region_stats(prompt_path, (0, 480, 1280, 240)) lines.append(f"p44-dialog-c prompt band (0,480,1280,240) dark_frac={df:.3f} maxblk={block} bright={bright}") if df >= DARK_FRAC_MAX: failures.append(f"prompt band dark_frac {df:.3f} >= {DARK_FRAC_MAX}") if block >= BLOCK * BLOCK: failures.append("prompt band has solid near-black block") if bright < 500: failures.append(f"prompt band bright {bright} < 500") else: failures.append("missing p44-dialog-c.png") lines.append("") def diff(a, b): ia = np.asarray(Image.open(rec / f"{a}.png").convert("RGB"), dtype=np.int16) ib = np.asarray(Image.open(rec / f"{b}.png").convert("RGB"), dtype=np.int16) return int((np.abs(ia - ib).sum(axis=2) > 0).sum()) for a, b in [("p43-hud-day", "p43-hud-night"), ("p44-dialog-a", "p44-dialog-c")]: if (rec / f"{a}.png").exists() and (rec / f"{b}.png").exists(): d = diff(a, b) lines.append(f"diff {a} vs {b}: differing_pixels={d}") if d == 0: failures.append(f"{a} identical to {b}") lines.append("") if failures: lines.append("verdict=FAIL") for f in failures: lines.append(" - " + f) else: lines.append("verdict=PASS") lines.append(" calendar/panel/prompt regions have no solid near-black block;") lines.append(" panel and dialog bottoms clear the hotbar top; frames differ pairwise.") out.parent.mkdir(parents=True, exist_ok=True) out.write_text("\n".join(lines) + "\n", encoding="utf-8") print(f"wrote {out} verdict={'FAIL' if failures else 'PASS'}") return 1 if failures else 0 if __name__ == "__main__": sys.exit(main())