summaryrefslogtreecommitdiff
path: root/scripts
diff options
context:
space:
mode:
Diffstat (limited to 'scripts')
-rw-r--r--scripts/analyze-p68.py189
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())