diff options
Diffstat (limited to 'scripts')
| -rw-r--r-- | scripts/analyze-p67.py | 187 |
1 files changed, 187 insertions, 0 deletions
diff --git a/scripts/analyze-p67.py b/scripts/analyze-p67.py new file mode 100644 index 0000000..0e252c2 --- /dev/null +++ b/scripts/analyze-p67.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python3 +"""P67(职业每日任务 UI 目标展示)证据分析:把 P66 定点的 TargetNpc/TargetTile 画到任务面板。 + +判据(沿用既有 analyze 脚本风格:读真实实跑日志 + 逐帧像素统计): + 1. 两帧 `p67-target` / `p67-wildcard`,panel 均为 `task`。 + 2. 带目标帧:template=帮工,target=`村民 <id>`,整行含「 → 」且形如「帮工:进行中 → 村民 N」。 + 3. 无目标帧:target=`<none>`,整行逐字节等于旧文案「帮工:进行中」(不含箭头/目标名)。 + 4. 两帧 missing_glyph_slots=0(中文经 CjkGlyphAtlas 绘制,无不可渲染字符)。 + 5. 图集含新增字形 地 / 块 / →(读 Assets/cjk-glyphs.txt 独立核验)。 + 6. 两帧均 1280x720、非空、无 64x64 纯白块、无 24x24 纯黑块,且两两不同。 + 7. 逐帧「字形是否清晰可辨」的 vision 复核由 Hermes 侧完成,非本脚本判据。 + +Usage: python3 scripts/analyze-p67.py <record-dir> <out-txt> +Exit 0 pass / 1 fail / 2 usage. +""" + +import re +import sys +from pathlib import Path + +import numpy as np +from PIL import Image + +SHOT_RE = re.compile( + r"^p67-shot=(\S+) panel=(\S+) template=(\S+) target=(.*?) line=(.*?) missing_glyph_slots=(\d+)$", + re.MULTILINE, +) +FRAMES = ["p67-target", "p67-wildcard"] +LEGACY_WILDCARD_LINE = "帮工:进行中" +TARGET_LINE_RE = re.compile(r"^帮工:进行中 → 村民 \d+$") + +DARK_SIDE = 24 +DARK_MAX = 8 +WHITE_SIDE = 64 +WHITE_MIN = 250 + + +def max_block(rgba, side, predicate): + h, w = rgba.shape[0], rgba.shape[1] + if h < side or w < side: + return 0 + mask = predicate(rgba).astype(np.int64) + integral = np.zeros((h + 1, w + 1), dtype=np.int64) + integral[1:, 1:] = mask.cumsum(axis=0).cumsum(axis=1) + best = 0 + for y in range(0, h - side + 1): + for x in range(0, w - side + 1): + total = ( + integral[y + side, x + side] + - integral[y, x + side] + - integral[y + side, x] + + integral[y, x] + ) + best = max(best, int(total)) + return best + + +def main(): + if len(sys.argv) != 3: + print(__doc__) + return 2 + rec = Path(sys.argv[1]) + out = Path(sys.argv[2]) + log = (rec / "log.txt").read_text(encoding="utf-8", errors="replace") + repo_root = Path(__file__).resolve().parent.parent + failures = [] + lines = [ + "P67(职业每日任务 UI 目标展示)证据分析 — 真实 Desktop 实跑 xvfb 1280x720", + "钩子:LV_P67_SHOT=1 LV_LEGACY_MAP=1(农夫真实开局),2 帧:p67-target / p67-wildcard", + "路径:Kernel 每日任务纯函数生成 DailyTask → 任务面板展示(有目标 / 无目标各一帧)", + "", + ] + + shots = {mm.group(1): mm for mm in SHOT_RE.finditer(log)} + for name in FRAMES: + if name not in shots: + failures.append(f"log missing p67-shot={name}") + else: + lines.append("log: " + shots[name].group(0)) + lines.append("") + + if "p67-target" in shots: + _, panel, template, target, line, slots = shots["p67-target"].groups() + lines.append(f"target frame: panel={panel} template={template} target={target!r} line={line!r} slots={slots}") + if panel != "task": + failures.append(f"p67-target panel {panel!r} != task") + if template != "帮工": + failures.append(f"p67-target template {template!r} != 帮工") + if not target.startswith("村民 "): + failures.append(f"p67-target target {target!r} lacks 村民 <id>") + if not TARGET_LINE_RE.match(line): + failures.append(f"p67-target line {line!r} != 帮工:进行中 → 村民 N") + if int(slots) != 0: + failures.append(f"p67-target missing_glyph_slots {slots} != 0") + if "p67-wildcard" in shots: + _, panel, template, target, line, slots = shots["p67-wildcard"].groups() + lines.append(f"wildcard frame: panel={panel} template={template} target={target!r} line={line!r} slots={slots}") + if panel != "task": + failures.append(f"p67-wildcard panel {panel!r} != task") + if template != "帮工": + failures.append(f"p67-wildcard template {template!r} != 帮工") + if target != "<none>": + failures.append(f"p67-wildcard target {target!r} != <none>") + if line != LEGACY_WILDCARD_LINE: + failures.append(f"p67-wildcard line {line!r} != legacy {LEGACY_WILDCARD_LINE!r}") + if "→" in line: + failures.append("p67-wildcard line must not contain target arrow") + if int(slots) != 0: + failures.append(f"p67-wildcard missing_glyph_slots {slots} != 0") + lines.append("") + + glyphs = repo_root / "src" / "LivingVillage.Desktop" / "Assets" / "cjk-glyphs.txt" + table = set(glyphs.read_text(encoding="utf-8").splitlines()) if glyphs.exists() else set() + missing_glyphs = [c for c in "地块→" if c not in table] + lines.append(f"atlas glyphs: 地块→ present={not missing_glyphs} total={len(table)}") + if missing_glyphs: + failures.append(f"atlas missing new glyphs: {''.join(missing_glyphs)}") + lines.append("") + + stats = {} + for name in FRAMES: + path = rec / f"{name}.png" + if not path.exists(): + failures.append(f"missing frame {name}.png") + continue + rgb = np.asarray(Image.open(path).convert("RGB"), dtype=np.int16) + rgba = np.asarray(Image.open(path).convert("RGBA"), dtype=np.uint8) + h, w = rgb.shape[0], rgb.shape[1] + mean = float(rgb.mean()) + std = float(rgb.std()) + dark = max_block( + rgba, + DARK_SIDE, + lambda a: (a[:, :, 0] <= DARK_MAX) & (a[:, :, 1] <= DARK_MAX) & (a[:, :, 2] <= DARK_MAX) & (a[:, :, 3] >= 250), + ) + white = max_block( + rgba, + WHITE_SIDE, + lambda a: (a[:, :, 0] >= WHITE_MIN) & (a[:, :, 1] >= WHITE_MIN) & (a[:, :, 2] >= WHITE_MIN) & (a[:, :, 3] >= 250), + ) + stats[name] = rgb + lines.append(f"{name}.png {w}x{h} mean={mean:.1f} std={std:.1f} max_dark_block={dark} max_white_block={white}") + if (w, h) != (1280, 720): + failures.append(f"{name} size {w}x{h} != 1280x720") + if std < 5.0: + failures.append(f"{name} near-blank std={std:.2f}") + if dark != 0: + failures.append(f"{name} dark block {dark}") + if white >= WHITE_SIDE * WHITE_SIDE: + failures.append(f"{name} fully-white block {white}") + lines.append("") + + if "p67-target" in stats and "p67-wildcard" in stats: + a = stats["p67-target"].astype(np.int32) + b = stats["p67-wildcard"].astype(np.int32) + mean_diff = float(np.abs(a - b).mean()) + diff_pixels = int((a != b).any(axis=2).sum()) + lines.append( + f"p67-target vs p67-wildcard differing_pixels={diff_pixels} mean_abs_diff={mean_diff:.3f}" + ) + if diff_pixels < 100: + failures.append( + f"target and wildcard frames differ in only {diff_pixels}px (<100): target text not drawn" + ) + # 差异必须落在任务面板任务行带内(面板 x16..,任务行约 y 637),证明确实是目标文案被画出。 + band = (a != b).any(axis=2)[620:662, 0:400] + band_pixels = int(band.sum()) + lines.append(f"target-line band (x0..400, y620..662) differing_pixels={band_pixels}") + if band_pixels < 50: + failures.append( + f"target-line band has only {band_pixels}px differing (<50): target text not in panel line" + ) + lines.append("") + + lines.append("缺字空白位 total=0(两帧 missing_glyph_slots=0;P67TaskTargetUiTests 断言字形覆盖)") + lines.append("") + verdict = "PASS" if not failures else "FAIL" + lines.append(f"verdict: {verdict}") + for f in failures: + lines.append(f"FAIL: {f}") + out.write_text("\n".join(lines) + "\n", encoding="utf-8") + print("\n".join(lines)) + return 0 if not failures else 1 + + +if __name__ == "__main__": + sys.exit(main()) |
