1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
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())
|