summaryrefslogtreecommitdiff
path: root/scripts
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-29 08:56:42 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-29 08:56:42 +0800
commita578791b0dd8ca95fba3af6f3c2f39c816bd7e52 (patch)
treeaf71c5a7d69087a59f5f5116134c602f65960ae0 /scripts
parent8cfdb2aa8b19595f5fa7870c4ad3e6c4c20c421e (diff)
downloadliving-village-a578791b0dd8ca95fba3af6f3c2f39c816bd7e52.tar.gz
p67: 每日任务面板展示目标(TargetNpc/TargetTile 可见化)
- TaskRuntime.targetText/taskLine:帮工→「村民 N」、翻土→「自家地块」, 无目标时与旧任务行文案逐字节一致(不加箭头/目标名) - Interaction TaskPanel 任务行改用 taskLine(其余面板/文案不动) - 字形表新增 地 块 →(343 字,图集重生成);ChineseText.requiredUiLabels 增补 - P67TaskTargetUiTests(5 例,红先)+ PrototypeTests 计数同步 340→343 - LV_P67_SHOT 证据钩子(有目标/无目标各一帧)+ scripts/analyze-p67.py - Desktop 333/333、Kernel 119/119;digest 953775FA…7F6C ×3 PASS;未动 Kernel/Sim
Diffstat (limited to 'scripts')
-rw-r--r--scripts/analyze-p67.py187
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())