diff options
Diffstat (limited to 'scripts')
| -rw-r--r-- | scripts/analyze-p68.py | 189 |
1 files changed, 189 insertions, 0 deletions
diff --git a/scripts/analyze-p68.py b/scripts/analyze-p68.py new file mode 100644 index 0000000..e2a573c --- /dev/null +++ b/scripts/analyze-p68.py @@ -0,0 +1,189 @@ +#!/usr/bin/env python3 +"""P68(512×384 整图渲染截图证据)分析:读 headless 离线渲染的整图 PNG + 实跑日志。 + +判据: + 1. 尺寸:PNG 宽高 = 512×scale × 384×scale(scale>=8),日志 `p68-map` 一致。 + 2. 河流/主路/农田/民居像素存在性:按 MapSnapshot 调色板精确计数 + (water / paddy+vegetable / road / path / bridge / roof / grove 均 > 0)。 + 3. 无纯白/纯黑大块(64x64 全白、24x24 全黑)。 + 4. missing_glyph_slots=0(本图为纯地图,无 UI 文案帧)。 + 5. 确定性:日志 `deterministic=true`。 + +Usage: python3 scripts/analyze-p68.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 + +MAP_RE = re.compile( + r"^p68-map width=(\d+) height=(\d+) scale=(\d+) png=(\d+)x(\d+) bytes=(\d+)$", + re.MULTILINE, +) +TILES_RE = re.compile( + r"^p68-tiles grass=(\d+) water=(\d+) stone=(\d+) peat=(\d+) paddy=(\d+) vegetable=(\d+)$", + re.MULTILINE, +) +FEATURES_RE = re.compile( + r"^p68-features rivers=(\d+) bridges=(\d+) paths=(\d+) buildings=(\d+) farmhouses=(\d+) groves=(\d+) road_columns=(\d+)$", + re.MULTILINE, +) +RENDER_RE = re.compile(r"^p68-render deterministic=(\w+) sha256=([0-9A-F]+)$", re.MULTILINE) + +PALETTE = { + "grass": (86, 140, 74), + "water": (52, 110, 180), + "stone": (150, 150, 150), + "peat": (110, 96, 72), + "paddy": (120, 168, 84), + "vegetable": (154, 186, 96), + "path": (196, 176, 120), + "road": (214, 190, 128), + "bridge": (140, 96, 64), + "roof": (58, 58, 70), + "door": (120, 72, 40), + "grove": (34, 96, 52), +} + +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") + failures = [] + lines = [ + "P68(512×384 整图渲染截图证据)分析 — headless 离线渲染(无图形设备)", + "钩子:LV_P68_MAPSHOT=1(MapGen.paramsForSize 512 384 seed=4242 → MapSnapshot 整图 PNG)", + "", + ] + + m = MAP_RE.search(log) + if not m: + failures.append("log missing p68-map") + else: + width, height, scale, pw, ph, nbytes = (int(v) for v in m.groups()) + lines.append(f"log: {m.group(0)}") + if (width, height) != (512, 384): + failures.append(f"map {width}x{height} != 512x384") + if scale < 8: + failures.append(f"scale {scale} < 8 (每瓦片像素不足)") + if (pw, ph) != (width * scale, height * scale): + failures.append(f"png {pw}x{ph} != {width * scale}x{height * scale}") + lines.append("") + + m = TILES_RE.search(log) + if not m: + failures.append("log missing p68-tiles") + tile_counts = None + else: + tile_counts = {k: int(v) for k, v in zip( + ["grass", "water", "stone", "peat", "paddy", "vegetable"], m.groups())} + lines.append("log: " + m.group(0)) + m = FEATURES_RE.search(log) + if not m: + failures.append("log missing p68-features") + else: + lines.append("log: " + m.group(0)) + lines.append("") + + m = RENDER_RE.search(log) + if not m: + failures.append("log missing p68-render") + else: + deterministic, sha = m.groups() + lines.append(f"log: {m.group(0)}") + if deterministic != "true": + failures.append("render not deterministic") + if len(sha) != 64: + failures.append(f"sha256 length {len(sha)} != 64") + lines.append("") + + png = rec / "p68-map.png" + if not png.exists(): + failures.append("missing p68-map.png") + else: + rgb = np.asarray(Image.open(png).convert("RGB"), dtype=np.uint8) + rgba = np.asarray(Image.open(png).convert("RGBA"), dtype=np.uint8) + h, w = rgb.shape[0], rgb.shape[1] + lines.append(f"p68-map.png {w}x{h}") + if (w, h) != (512 * 8, 384 * 8): + failures.append(f"decoded png {w}x{h} != 4096x3072") + flat = rgb.reshape(-1, 3) + counts = {} + for name, color in PALETTE.items(): + counts[name] = int((flat == np.array(color, dtype=np.uint8)).all(axis=1).sum()) + lines.append("palette pixel counts: " + " ".join(f"{k}={v}" for k, v in counts.items())) + for name in ("water", "paddy", "vegetable", "road", "path", "bridge", "roof", "grove"): + if counts[name] == 0: + failures.append(f"missing {name} pixels") + if counts["paddy"] + counts["vegetable"] == 0: + failures.append("missing farmland (paddy+vegetable) pixels") + if counts["roof"] == 0: + failures.append("missing building roof pixels") + if tile_counts is not None and tile_counts["water"] > 0: + ratio = counts["water"] / (tile_counts["water"] * 64) + lines.append(f"water pixel ratio (vs tile count) = {ratio:.3f}") + if ratio < 0.5: + failures.append(f"water ratio {ratio:.3f} < 0.5 (river pixels missing)") + 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), + ) + lines.append(f"max_black_block={dark} max_white_block={white}") + if dark != 0: + failures.append(f"black block {dark}") + if white >= WHITE_SIDE * WHITE_SIDE: + failures.append(f"fully-white block {white}") + lines.append("") + + lines.append("missing_glyph_slots=0(纯地图整图,无 UI 文案帧)") + 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()) |
