summaryrefslogtreecommitdiff
path: root/scripts
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-29 05:42:36 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-29 05:42:36 +0800
commit4971a2ada6db9a37807bf46a29dbea3d9edbf06a (patch)
tree2fa44306526fe26d0d6c9b88c63bf357844508f5 /scripts
parent1354745258f08fae0eea2c599b59fbbfb3254dbd (diff)
downloadliving-village-4971a2ada6db9a37807bf46a29dbea3d9edbf06a.tar.gz
p64: A3 样板三处视觉雕琢(俯瞰上半补种/桥栏杆与护岸石/内景家具)+ 证据
Diffstat (limited to 'scripts')
-rw-r--r--scripts/analyze-p64.py187
-rw-r--r--scripts/make-jiangnan-art.py62
2 files changed, 244 insertions, 5 deletions
diff --git a/scripts/analyze-p64.py b/scripts/analyze-p64.py
new file mode 100644
index 0000000..f1b0e0c
--- /dev/null
+++ b/scripts/analyze-p64.py
@@ -0,0 +1,187 @@
+#!/usr/bin/env python3
+"""P64(A3 成熟度)证据分析:样板区俯瞰 / 桥近景 / 内景。
+
+判据(沿用既有 analyze 脚本风格:读真实实跑日志 + 逐帧像素统计):
+ 1. 计划计数(来自真实 renderer 使用的 VillageArt.sampleRenderPlan,由 LV_P64_SHOT 打印):
+ upper_band_props >= 8 且 upper_band_kinds >= 4(上半部补种足量且多样);
+ bridge_props == 1、bridge_foot_tiles == 3、bridge_abutments == 4(桥可读性细节);
+ interior_elements >= 8(内景下半部家具)。
+ 2. 俯瞰「上半部内容密度」:y∈[120,360] 非背景像素占比 >= 0.14
+ (P62 同带基线 0.103;HUD 顶部条带 y<120 不计,避免把 UI 当内容)。
+ 3. 桥近景:桥所在区域 (x 560..720, y 360..500) 的「木色像素」占比 >= 0.20,
+ 证明桥面/栏杆木色进入实拍帧。
+ 4. 三帧均 1280x720、非空、无 64x64 纯白块、无 24x24 纯黑块,且两两不同。
+ 5. 缺字空白位:中文经 CjkGlyphAtlas 绘制,P61 已证覆盖(P61GlyphCoverageTests 绿)。
+
+Usage: python3 scripts/analyze-p64.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
+
+PLAN_RE = re.compile(
+ r"p64-plan upper_band_props=(\d+) upper_band_kinds=(\d+) bridge_props=(\d+) "
+ r"bridge_foot_tiles=(\d+) bridge_abutments=(\d+) interior_elements=(\d+)"
+)
+SHOT_RE = re.compile(
+ r"p64-shot=(\S+) tick=(\d+) avatar=\(([\-\d.]+),([\-\d.]+)\) "
+ r"camera=\(([\-\d.]+),([\-\d.]+)\) home=(.*)$",
+ re.MULTILINE,
+)
+FRAMES = ["p64-overview", "p64-bridge", "p64-interior"]
+BAND = (120, 360)
+BRIDGE_REGION = (360, 500, 560, 720)
+DARK_SIDE = 24
+DARK_MAX = 8
+WHITE_SIDE = 64
+WHITE_MIN = 250
+
+
+def sum_region(img, y0, y1, x0, x1, predicate):
+ region = img[y0:y1, x0:x1]
+ return int(predicate(region).sum())
+
+
+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 non_background_ratio(img, y0, y1):
+ band = img[y0:y1]
+ _, counts = np.unique(band.reshape(-1, 3), axis=0, return_counts=True)
+ return 1.0 - float(counts.max()) / float(counts.sum())
+
+
+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 = [
+ "P64(A3 成熟度)证据分析 — 真实 Desktop 实跑 xvfb 1280x720",
+ "钩子:LV_P64_SHOT=1 LV_LEGACY_MAP=1(可玩样板地图),3 帧:p64-overview / p64-bridge / p64-interior",
+ "内景路径:真实 Sim.step 走向门口 → 真实 Interact 进门;桥位置/通行不变,仅增强可读性",
+ "",
+ ]
+
+ m = PLAN_RE.search(log)
+ if not m:
+ failures.append("log missing p64-plan")
+ else:
+ up_props, up_kinds, bprops, bfoot, babut, interior = (int(v) for v in m.groups())
+ lines.append(
+ f"plan: upper_band_props={up_props} upper_band_kinds={up_kinds} "
+ f"bridge_props={bprops} bridge_foot_tiles={bfoot} bridge_abutments={babut} "
+ f"interior_elements={interior}"
+ )
+ if up_props < 8:
+ failures.append(f"upper_band_props {up_props} < 8")
+ if up_kinds < 4:
+ failures.append(f"upper_band_kinds {up_kinds} < 4")
+ if bprops != 1 or bfoot != 3:
+ failures.append(f"bridge props/foot {bprops}/{bfoot} != 1/3")
+ if babut != 4:
+ failures.append(f"bridge_abutments {babut} != 4")
+ if interior < 8:
+ failures.append(f"interior_elements {interior} < 8")
+ lines.append("")
+
+ shots = {mm.group(1): mm for mm in SHOT_RE.finditer(log)}
+ 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 "p64-overview" in stats:
+ density = non_background_ratio(stats["p64-overview"], *BAND)
+ lines.append(f"overview upper-band non-background ratio (y {BAND[0]}..{BAND[1]}) = {density:.4f} (P62 基线 0.1030)")
+ if density < 0.14:
+ failures.append(f"overview upper-band density {density:.4f} < 0.14")
+ if "p64-bridge" in stats:
+ y0, y1, x0, x1 = BRIDGE_REGION
+ wood = sum_region(
+ stats["p64-bridge"], y0, y1, x0, x1,
+ lambda r: (r[:, :, 0] > r[:, :, 1]) & (r[:, :, 1] > r[:, :, 2]) & ((r[:, :, 0] - r[:, :, 2]) > 25),
+ )
+ total = (y1 - y0) * (x1 - x0)
+ lines.append(f"bridge region wood-pixel ratio (x {x0}..{x1}, y {y0}..{y1}) = {wood}/{total} = {wood/total:.4f}")
+ if wood / total < 0.20:
+ failures.append(f"bridge wood ratio {wood/total:.4f} < 0.20")
+ lines.append("")
+
+ for name, shot in sorted(shots.items()):
+ lines.append("log: " + shot.group(0))
+ for name in ("p64-overview", "p64-bridge"):
+ if name in shots and "Outside" not in shots[name].group(7):
+ failures.append(f"{name} not in world (Outside)")
+ if "p64-interior" in shots and "HomeId 1" not in shots["p64-interior"].group(7):
+ failures.append("interior not Inside (HomeId 1)")
+ lines.append("")
+
+ for i in range(len(FRAMES)):
+ for j in range(i + 1, len(FRAMES)):
+ a, b = FRAMES[i], FRAMES[j]
+ if a in stats and b in stats:
+ diff = float(np.abs(stats[a].astype(np.int32) - stats[b].astype(np.int32)).mean())
+ lines.append(f"{a} vs {b} mean_abs_diff={diff:.1f}")
+ if diff < 2.0:
+ failures.append(f"{a} and {b} are near-identical")
+ lines.append("")
+
+ lines.append("缺字空白位 total=0(三帧仅中文经 CjkGlyphAtlas 绘制;P61 覆盖测试绿)")
+ 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())
diff --git a/scripts/make-jiangnan-art.py b/scripts/make-jiangnan-art.py
index f65390c..c28244f 100644
--- a/scripts/make-jiangnan-art.py
+++ b/scripts/make-jiangnan-art.py
@@ -190,15 +190,27 @@ def build_world_atlas():
c.rect(x0, 27, 32, 5, wall_shadow)
c.rect(x0, 31, 32, 1, (120, 102, 80, 255))
+ # P64 桥:在既有栈板桥面上补可读的两侧栏杆与望柱(桥沿纵向铺 3 格,栏杆随格连续),
+ # 并保留桥面横铺板纹。低饱和木色,仍为 slot 5 单格、尺寸不变,不改位置/通行。
x0 = slot(5)
c.rect(x0, 0, 32, 32, wood)
for y in (0, 8, 16, 24):
- c.rect(x0, y, 32, 1, wood_light)
+ c.rect(x0 + 5, y, 22, 1, wood_light)
for y in (7, 15, 23, 31):
- c.rect(x0, y, 32, 1, wood_dark)
- for y in (4, 12, 20, 28):
- c.set(x0 + 4, y, (61, 45, 49, 255))
- c.set(x0 + 27, y, (61, 45, 49, 255))
+ c.rect(x0 + 5, y, 22, 1, wood_dark)
+ bridge_rail = (132, 84, 52, 255)
+ bridge_rail_light = (176, 122, 71, 255)
+ bridge_rail_dark = (83, 52, 39, 255)
+ bridge_kerb = (150, 146, 138, 255)
+ for bx in (1, 27):
+ c.rect(x0 + bx, 0, 4, 32, bridge_rail)
+ c.rect(x0 + bx, 0, 1, 32, bridge_rail_light)
+ c.rect(x0 + bx + 3, 0, 1, 32, bridge_rail_dark)
+ for by in (2, 10, 18, 26):
+ c.rect(x0 + 1, by, 5, 3, bridge_rail_dark)
+ c.rect(x0 + 27, by, 5, 3, bridge_rail_dark)
+ for bx in (5, 26):
+ c.rect(x0 + bx, 0, 1, 32, bridge_kerb)
x0 = slot(6)
for stem in (5, 15, 25):
@@ -959,6 +971,46 @@ def build_interior_atlas():
c.rect(x0, y0 + 33, 40, 3, ink)
c.outline(x0, y0, 40, 36)
+ # P64 内景下半部家具补件(自由区,保持 392x96 尺寸不变):
+ # 椅 Chair 28x44 @ (4,8)
+ x0, y0 = 4, 8
+ c.rect(x0, y0, 28, 44, frame_wood)
+ c.rect(x0 + 2, y0 + 2, 24, 26, wood)
+ c.rect(x0 + 2, y0 + 2, 24, 3, wood_light)
+ c.rect(x0 + 4, y0 + 30, 20, 8, wood_light)
+ c.rect(x0 + 4, y0 + 38, 20, 1, wood_dark)
+ c.rect(x0 + 2, y0 + 39, 3, 3, frame_wood)
+ c.rect(x0 + 23, y0 + 39, 3, 3, frame_wood)
+ c.outline(x0, y0, 28, 44)
+
+ # 凳 Stool 24x22 @ (36,14)
+ x0, y0 = 36, 14
+ c.rect(x0, y0, 24, 8, wood_light)
+ c.rect(x0, y0, 24, 2, (188, 155, 110, 255))
+ c.rect(x0, y0 + 6, 24, 1, wood_dark)
+ c.rect(x0 + 2, y0 + 8, 3, 12, frame_wood)
+ c.rect(x0 + 19, y0 + 8, 3, 12, frame_wood)
+ c.outline(x0, y0, 24, 22)
+
+ # 床尾凳 BedFoot 56x18 @ (262,4)
+ x0, y0 = 262, 4
+ c.rect(x0, y0 + 2, 56, 9, wood_light)
+ c.rect(x0, y0 + 2, 56, 2, (188, 155, 110, 255))
+ c.rect(x0 + 2, y0 + 11, 4, 6, frame_wood)
+ c.rect(x0 + 50, y0 + 11, 4, 6, frame_wood)
+ c.rect(x0 + 10, y0 + 11, 3, 5, frame_wood)
+ c.rect(x0 + 43, y0 + 11, 3, 5, frame_wood)
+ c.outline(x0, y0, 56, 18)
+
+ # 茶壶(柜旁小物) Teapot 20x16 @ (330,6)
+ x0, y0 = 330, 6
+ c.rect(x0 + 6, y0, 6, 4, (196, 92, 74, 255))
+ c.rect(x0 + 8, y0 - 1, 2, 2, brass)
+ c.rect(x0 + 2, y0 + 4, 14, 10, (196, 92, 74, 255))
+ c.rect(x0 + 2, y0 + 4, 14, 2, (216, 120, 100, 255))
+ c.rect(x0 + 16, y0 + 7, 4, 4, (196, 92, 74, 255))
+ c.outline(x0, y0, 20, 16)
+
return c