merge: M-1 迭代 2「样张冻结与表现定稿」loop——样张 v2 机检 16/16 四冻结+框材组 8 件接线+演出密度终稿+棋盘数字终稿+M-1 出口闸检查单

五批次 a08d89c/f9b0765→2d1388b:①T1 样张 v2(机检闸形 A 全过即冻结——新增 A3 Δ≥55/F6 双明度带/F7 DEPTH_PX 同源/A6 观感密度四断言,art-bible v2.13 落四冻结+UiKit 小人蓝同步 #2E86DE+图例旗色封印黑漏网修)②T2 框材组 8 件(♻ Kenney/OGA 全 CC0 量化改色入包,UiKit 单点工厂+五视图接线,✦ 背景横幅列缺料)③T3 演出四项(死亡碎裂/胜利纹章/失败掸灰/意图三档+结算光带缘线)+fx_probe 新探针④T4 数字形制冻结值落码(三档墨色温/字号 ≥18 公式/色牌 0.60·0.62)⑤T5 M-1 出口闸检查单(design/qa/m1-exit-gate-checklist-2026-09-28.md——真人目检八项+✦ 缺料清单)+06 两已解决条目移除+roadmap 挂起清单对齐。
验证:样张机检 16/16+冒烟 557/0(基线持平)+五探针(ui_input/ui_probe/ui_click/ui_tune/fx 新增)全 PASS+doc 三闸对基线零新增(lint 8=8/term 215=215/死链 0)+熵增闸 PASS。
This commit is contained in:
DKChan committed 2026-09-28 04:58:58 +08:00
commit 9abf5ff448
20 files changed
+748 -76

No files matched your search

+116
View File
@@ -0,0 +1,116 @@
# -*- coding: utf-8 -*-
"""UI 框材组量化改色脚本(M-1 迭代 2 T2,2026-09-28)。
♻ 复用件 → 复用闸①色板量化:全部产物按明度重映射到 art-bible §4.1/§7.2 token 渐变带,
不保留原包杂色;闸②禁入带=token 色天然合规;闸③灰度可辨=图标四形互异;闸④授权=全 CC0。
源包 = artwork/ui-reuse-2026-09-23/(素材不入 git——先查本仓库,缺位回退主检出 D:/code/g-plan2):
oga-kenney-pixel-ui-750(CC0,OGA) 9-Slice/Ancient 面板/按钮九宫格 48×48
kenney-fantasy-ui-borders(CC0) panel-border 石框 48×48
5-石纹底噪/stone_6.png(CC0,OGA Handpainted stone) 无缝底噪 1200×1200
kenney-board-game-icons(CC0) 64px 图标四件
产物 → game/assets/art/ui/(终稿素材目录,不入 git;消费 = UiKit.load_tex 守卫,缺位程序化回退)。
用法:python tools/quantize_ui_skin.py
"""
from PIL import Image
import os
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
MAIN_ROOT = "D:/code/g-plan2"
SRC = ("artwork", "ui-reuse-2026-09-23")
OUT = ("game", "assets", "art", "ui")
def src_path(*parts):
for base in (ROOT, MAIN_ROOT):
p = os.path.join(base, *SRC, *parts)
if os.path.exists(p):
return p
raise FileNotFoundError("/".join(SRC + parts))
def out_path(name):
p = os.path.join(ROOT, *OUT, name)
os.makedirs(os.path.dirname(p), exist_ok=True)
return p
# §4.1/§7.2 token 渐变带(量化目标值——样张 v2 冻结色板)
RAMP_PANEL = [(120, 106, 88), (226, 210, 182), (243, 234, 214)] # PANEL_BORDER→PANEL_DATA→PLATE 微亮
RAMP_BUTTON = [(179, 154, 120), (201, 175, 131), (245, 234, 211)] # FLOOR→BRICK(掩尘"可按的砖")→PLATE
RAMP_PRESSED = [(120, 106, 88), (179, 154, 120)] # PANEL_BORDER→FLOOR
RAMP_BACKDROP = [(30, 25, 20), (120, 106, 88)] # BG_ROOT→PANEL_BORDER(低对比自然后退 §6.1)
RAMP_BORDER = [(120, 106, 88), (201, 175, 131)] # PANEL_BORDER→BRICK
ICON_GOLD = (240, 168, 40) # 奖励金(钥石)
ICON_WARM = (240, 226, 200) # 暖白(柜台件,暗卡上可读)
def ramp_color(ramp, t):
if t <= 0:
return ramp[0]
if t >= 1:
return ramp[-1]
seg = t * (len(ramp) - 1)
i = min(int(seg), len(ramp) - 2)
k = seg - i
a, b = ramp[i], ramp[i + 1]
return tuple(int(a[j] + (b[j] - a[j]) * k) for j in range(3))
def remap_luma(img, ramp, gamma=1.0):
"""明度→token 渐变带重映射(保留 alpha 与纹理结构——闸①量化)。"""
im = img.convert("RGBA")
px = im.load()
vals = [sum(px[x, y][:3]) / 3.0 for y in range(im.size[1]) for x in range(im.size[0])
if px[x, y][3] > 8]
lo, hi = min(vals), max(vals)
span = max(1.0, hi - lo)
for y in range(im.size[1]):
for x in range(im.size[0]):
r, g, b, a = px[x, y]
if a <= 8:
continue
t = ((sum((r, g, b)) / 3.0) - lo) / span
px[x, y] = ramp_color(ramp, t ** gamma) + (a,)
return im
def recolor_flat(img, rgb):
"""剪影图标单色重着色(保留 alpha 抗锯齿——形状承载识别,闸③)。"""
im = img.convert("RGBA")
px = im.load()
for y in range(im.size[1]):
for x in range(im.size[0]):
r, g, b, a = px[x, y]
if a > 0:
px[x, y] = rgb + (a,)
return im
def main():
jobs = []
# 面板/按钮九宫格(pixel-ui-750 Ancient;texture_margin 取 8px——消费侧同值)
jobs.append(("panel_stone.png", remap_luma(Image.open(src_path("1-UI面板与按钮", "oga-kenney-pixel-ui-750(CC0)", "9-Slice", "Ancient", "brown.png")), RAMP_PANEL)))
jobs.append(("button_stone.png", remap_luma(Image.open(src_path("1-UI面板与按钮", "oga-kenney-pixel-ui-750(CC0)", "9-Slice", "Ancient", "tan.png")), RAMP_BUTTON)))
jobs.append(("button_stone_pressed.png", remap_luma(Image.open(src_path("1-UI面板与按钮", "oga-kenney-pixel-ui-750(CC0)", "9-Slice", "Ancient", "tan_pressed.png")), RAMP_PRESSED)))
# 石框(fantasy-ui-borders)
jobs.append(("border_stone.png", remap_luma(Image.open(src_path("1-UI面板与按钮", "kenney-fantasy-ui-borders(CC0)", "PNG", "Default", "Border", "panel-border-000.png")), RAMP_BORDER)))
# 石纹底噪(seamless 1200→256,暗带低对比——B 区/开始层背景)
stone = Image.open(src_path("5-石纹底噪", "stone_6.png")).convert("RGB").resize((256, 256), Image.LANCZOS)
jobs.append(("backdrop_stone.png", remap_luma(stone, RAMP_BACKDROP, gamma=1.6)))
# 营地图标四件(board-game-icons 64px;钥石=奖励金,柜台件=暖白)
icon_dir = ("3-节点图与营地图标", "kenney-board-game-icons(CC0)", "PNG", "Default (64px)")
jobs.append(("icon_keystone.png", recolor_flat(Image.open(src_path(*(icon_dir + ("suit_diamonds.png",)))), ICON_GOLD)))
jobs.append(("icon_forge.png", recolor_flat(Image.open(src_path(*(icon_dir + ("sword.png",)))), ICON_WARM)))
jobs.append(("icon_workshop.png", recolor_flat(Image.open(src_path(*(icon_dir + ("flask_full.png",)))), ICON_WARM)))
jobs.append(("icon_atlas.png", recolor_flat(Image.open(src_path(*(icon_dir + ("book_open.png",)))), ICON_WARM)))
for name, im in jobs:
im.save(out_path(name))
print("saved:", "/".join(OUT + (name,)))
print("=== 量化改色完成(%d 件,全 CC0;闸①②③④ 见脚本头注) ===" % len(jobs))
return 0
if __name__ == "__main__":
import sys
sys.exit(main())
+173 -23
View File
@@ -1,31 +1,37 @@
# -*- coding: utf-8 -*-
"""战斗一屏样张验收图生成脚本 v1(2026-09-26,循环 1 T1)。
"""战斗一屏样张验收图生成脚本 v2(2026-09-28,M-1 迭代 2 T1)。
v0 四 FATAL 案底(2026-09-26 用户否决)全部断言化——生成即验,不过闸不出图:
v1 用户否决四原因(2026-09-28)全部断言化——生成即验,不过闸不出图:
F1 未开格 = 方案甲缘件 8 邻拼装(词表查表,禁整块 center 铺满)
F2 旗仅出现于未开格
F3 小人脚锚 = 自底边 25% 线(⚠ 纵向比例一律自底边口径)
F4 数字 0 格无字形(纯安全格 = 干净裸地面)
F5 微光仅带状:接触带微光只画英雄格周界空间侧;类型微光禁整格染色(中心 50% 区零像素)
程序化四件审计(§9 初级验收授权):
F5 微光仅带状:接触带微光只画英雄格周界空间侧;类型微光禁整格染色;微光格零附加图形(角嵌退役)
F6 墙格顶面/立面双明度带(否决③立体感——南向临地墙格 Δ≥18,体积感机器代理)
F7 整板悬崖 DEPTH_PX 与 game 渲染常量同源(board_view.gd 解析——否决③立体感)
程序化审计(§9 初级验收授权 + 迭代 2 扩展):
A1 色板对表圣经冻结色板 + 禁入带(HSV 300°-345°)全图扫描
A2 拼缝周界闭合(墙↔地边界墙侧 2px 墨线带亮度 <70)
A3 明度对比(数字墨×色牌 WCAG ≥4.5:1;图底分离)
A3 明度对比(数字墨×色牌 WCAG ≥4.5:1;图底分离 Δ≥55——否决③,v1 实测 61.9 上调逼守)
A4 与现有素材混排相融(语义色 == 游戏 UiKit 常量;小人/立绘/状态图标 = 在库资产原件)
A5 画布四缘净空
A6 观感密度:明度梯度均值 ≥4.70(否决①「显示太丑」总纲代理——v1 同口径实测 4.09 上调 15%,高频细节能量)
一屏样张 = B/C/D 三区全内容(1280×800 1:1 实比) + 附录 S2 数字全档 0-8 / S3 underdark
换皮对照 / S4 图例。内容清单 = art-bible §1.4(九组,缺项即无效)。
素材源 = game/assets/art(board 21 瓦片 + character 小人/立绘)+ ui/icons 状态图标;
素材不入 git——先查本仓库,缺位回退主检出(D:/code/g-plan2)。
词表事实源 = design/research/board-tile-vocab-plan-a-2026-09-26.md;画法参数事实源 =
artwork/mockups/画法参数四源对照表-2026-09-26.md(四源对表先行产物)。
输出 = artwork/mockups/sample-batch-v1-2026-09-26.png + sample-v1-机检报告-2026-09-26.md。
design/research/画法参数四源对照表-2026-09-26.md(四源对表先行产物)。
输出 = artwork/mockups/sample-batch-v2-2026-09-28.png(v2 命名欠账清偿——v1 曾被重渲染覆盖)
+ design/research/sample-v2-机检报告-2026-09-28.md。
用法:python tools/render_sample_sheet.py
"""
from PIL import Image, ImageDraw, ImageFilter, ImageChops, ImageFont
import os
import sys
import math
import re
import hashlib
from collections import deque
@@ -45,8 +51,8 @@ def asset_root(*parts):
BOARD_DIR = ("game", "assets", "art", "board")
CHAR_DIR = ("game", "assets", "art", "character")
ICON_DIR = ("game", "assets", "ui", "icons")
OUT_PNG = os.path.join(ROOT, "artwork", "mockups", "sample-batch-v1-2026-09-26.png")
OUT_REPORT = os.path.join(ROOT, "artwork", "mockups", "sample-v1-机检报告-2026-09-26.md")
OUT_PNG = os.path.join(ROOT, "artwork", "mockups", "sample-batch-v2-2026-09-28.png")
OUT_REPORT = os.path.join(ROOT, "design", "research", "sample-v2-机检报告-2026-09-28.md")
T_FONT = asset_root("artwork", "ui-reuse-2026-09-23",
"4-字体", "fusion-pixel-12px(OFL)", "fusion-pixel-12px-proportional-latin.ttf")
ZH_FONT = r"C:\Windows\Fonts\msyh.ttc"
@@ -216,6 +222,31 @@ def floor_variant(x, y):
return int(hashlib.md5(("floor%d_%d" % (x, y)).encode()).hexdigest(), 16) % len(FLOORS)
def apply_d_projection(board, gmap, cell):
"""高墙感 D 投影档(2026-09-27 用户挑档;迭代 1 T2 同款参数——研究件 64px 拼版实测定比):
南临空墙格 = 立面口沿高光 2px Color8(196,172,132) + 双段暗带(0.16/0.25 格边,黑罩 α0.60/0.82);
北邻墙地面格 = 三段渐隐投影(顶带 0.22 格边,α0.28 起)。立体断言 F6/A6 的渲染承载。"""
overlay = Image.new("RGBA", board.size, (0, 0, 0, 0))
d = ImageDraw.Draw(overlay)
HILITE = (196, 172, 132)
seg1, seg2 = int(cell * 0.16), int(cell * 0.25)
for y in range(gmap.h):
for x in range(gmap.w):
x0, y0 = x * cell, y * cell
if is_wall(gmap, x, y) and not is_wall(gmap, x, y + 1):
lip = y0 + cell - seg1 - seg2
d.rectangle([x0, lip, x0 + cell, lip + 2], fill=HILITE + (255,))
d.rectangle([x0, lip + 2, x0 + cell, lip + 2 + seg1], fill=(0, 0, 0, 153))
d.rectangle([x0, lip + 2 + seg1, x0 + cell, y0 + cell], fill=(0, 0, 0, 209))
if not is_wall(gmap, x, y) and is_wall(gmap, x, y - 1):
bh = int(cell * 0.22)
for i, a in enumerate([71, 36, 14]):
d.rectangle([x0, y0 + i * (bh // 3), x0 + cell, y0 + (i + 1) * (bh // 3)],
fill=(20, 14, 8, a))
board.alpha_composite(overlay)
return board
# ══ D 区 10×10 样张板(内容布置见对照表 §6/§1.4 内容清单) ══
BOARD_ROWS = [
"WWWWWWWWWW",
@@ -239,7 +270,7 @@ HERO_DIGIT_AT = (2, 5) # 小人与数字同格(6)——验蓝框不覆盖文
NEST_AT = (5, 4) # 雷格巢穴(已开,程序占位+危险红)
GLOW_CELLS = [(4, 2, "attack"), (5, 2, "heal"), (8, 3, "blast"),
(6, 4, "pierce"), (7, 6, "magic")] # 五组;attack+heal 相邻并列
INLAY_RESULT = {(4, 6): "attack", (5, 6): "gp"} # 已开结果格物件纹样
INLAY_RESULT = {(4, 6): "attack", (5, 6): "gp", (6, 6): "heal", (3, 7): "defense"} # 已开结果格物件纹样(v2 增至四类型)
DIGITS = {(3, 4): 3, (3, 5): 1, (4, 5): 2, (2, 5): 6, (7, 4): 8, (7, 5): 7,
(4, 6): 5, (5, 6): 4} # 0 格=已开无字形(纯安全格全体现)
VARIANT_FLOOR_AT = (4, 5) # 装饰变体底色牌示例(强制 floor 变体 3)
@@ -248,6 +279,67 @@ CELL_D = 45 # 1280 基座 10×10 板实比(art-bible §8.1)
HERO_PX = 48 # 32 源 ×1.5(台账 161 档)
GROUND_FRAC = 0.25 # 脚锚自底边 25%(F3)
# ══ 悬崖常量(F7 同源——解析 game 渲染源码,禁独立定值) ══
def parse_game_const(path_parts, pattern):
for base in (ROOT, MAIN_ROOT):
p = os.path.join(base, *path_parts)
if os.path.exists(p):
m = re.search(pattern, open(p, encoding="utf-8").read())
if m:
return float(m.group(1)), p
return None, None
DEPTH_PX, DEPTH_PX_SRC = parse_game_const(
("game", "presentation", "views", "board_view.gd"), r"const DEPTH_PX\s*:?=\s*([0-9.]+)")
DENSITY_MIN = 4.70 # 观感密度下限(明度梯度均值)= v1 同口径实测 4.09 × 1.15(否决①总纲代理,迭代 2 白名单校准)
FACADE_MIN = 18.0 # 顶面/立面双明度带下限(否决③体积感机器代理)
WALL_FLOOR_MIN = 55 # 图底分离下限(v1 断言 40→上调,v1 实测 61.9)
def _gd_darkened(c, k):
return tuple(int(v * (1.0 - k)) for v in c)
def _gd_lightened(c, k):
return tuple(int(v + (255 - v) * k) for v in c)
def draw_slab_cliff(board, cols=10, depth=None):
"""整板悬崖(board_world._draw 同款结构件镜像——同源 DEPTH_PX,F7)。
右/下各延 depth px:正面暗带+右侧暗带+列缝+岩层横纹+口沿高光+接触 AO+底线+悬浮影。"""
d = int(round(depth if depth is not None else (DEPTH_PX or 8.0)))
w, h = board.size
out = Image.new("RGBA", (w + d, h + d), (0, 0, 0, 255))
out.paste(board, (0, 0))
dr = ImageDraw.Draw(out)
brick = UIKIT["BRICK"]
front = _gd_darkened(brick, 0.55) # 正面悬崖(背光)——board_world.gd:88
right = _gd_darkened(brick, 0.38) # 右侧悬崖(微受光)
dr.rectangle([0, h, w - 1, h + d], fill=front)
dr.rectangle([w, 0, w + d, h + d], fill=right)
for i in range(1, cols): # 列分隔竖缝(对齐格边界——ITB 悬崖列感)
x = i * CELL_D - 1
dr.rectangle([x, h, x + 2, h + d], fill=_gd_darkened(front, 0.25))
dr.rectangle([0, h + d * 0.38, w, h + d * 0.38 + 1.5], fill=_gd_darkened(front, 0.18)) # 岩层横纹
dr.rectangle([0, h + d * 0.72, w, h + d * 0.72 + 1.5], fill=_gd_darkened(front, 0.12))
dr.rectangle([0, h, w, h + 1], fill=_gd_lightened(brick, 0.18)) # 口沿高光(倒角受光)
dr.rectangle([w, 0, w + 1, h], fill=_gd_lightened(brick, 0.12))
ao = Image.new("RGBA", out.size, (0, 0, 0, 0)) # 顶面接触 AO
ImageDraw.Draw(ao).rectangle([0, h - 2, w - 1, h], fill=(0, 0, 0, 46))
ImageDraw.Draw(ao).rectangle([w - 2, 0, w, h], fill=(0, 0, 0, 36))
out.alpha_composite(ao)
dr.rectangle([0, h + d - 2, w + d, h + d], fill=_gd_darkened(front, 0.35)) # 底线收边
if d >= 8: # 悬浮影三层渐隐(中厚档起——board_world.gd:109)
for i, a in enumerate([0.16, 0.10, 0.05]):
sh = 3.0 + i * 3.0
sl = Image.new("RGBA", out.size, (0, 0, 0, 0))
ImageDraw.Draw(sl).rectangle(
[sh, h + d + sh * 0.5, w + d - sh * 1.5, h + d + sh * 0.5 + 2 + i],
fill=(0, 0, 0, int(255 * a)))
out.alpha_composite(sl)
return out
def wcag_lum(rgb):
def lin(c):
@@ -317,13 +409,14 @@ def draw_inlay(img, ox, oy, cell, kind, corner=4, m=None, L=None):
def draw_type_glow(glow_layer, ox, oy, cell, kind):
"""类型常显微光=内缘带状泛光(§4.2 色带;F5b 禁整格染色——中心 50% 区零像素)。"""
"""类型常显微光=内缘带状泛光(§4.2 色带;F5b 禁整格染色——中心区零像素)。
v2:泛光提亮 α170/带宽 6——观感密度(A6)层次承载,亮度不压格底明度档(§2 微光例外)。"""
layer = Image.new("RGBA", glow_layer.size, (0, 0, 0, 0))
d = ImageDraw.Draw(layer)
rgb = GLOW_GROUPS[kind]
band = max(3, cell // 11)
band = max(4, cell // 8)
d.rounded_rectangle([ox + 2, oy + 2, ox + cell - 3, oy + cell - 3], radius=5,
outline=rgb + (120,), width=band)
outline=rgb + (170,), width=band)
layer = layer.filter(ImageFilter.GaussianBlur(1))
glow_layer.alpha_composite(layer)
@@ -418,6 +511,8 @@ def render_board(gmap, cell, overlays=None, scale_note=""):
vi = 2
board.paste(FLOORS[vi].resize((cell, cell), Image.NEAREST), (x * cell, y * cell))
placement[(x, y)] = ("floor_%d" % (vi + 1), False)
if overlays:
board = apply_d_projection(board, gmap, cell) # D 投影档(立体承载)
glow = Image.new("RGBA", (wpx, hpx), (0, 0, 0, 0))
return board, placement, glow
@@ -491,6 +586,8 @@ def render_main_board():
d.line([(i * cell, 0), (i * cell, gmap.h * cell)], fill=(0, 0, 0, 45), width=1)
for i in range(gmap.h + 1):
d.line([(0, i * cell), (gmap.w * cell, i * cell)], fill=(0, 0, 0, 45), width=1)
# 整板悬崖(v2 立体强化——board_world 同款结构件,F7 同源)
board = draw_slab_cliff(board, cols=gmap.w)
return board, placement, records, band, gmap
@@ -645,18 +742,19 @@ def run_checks(board, placement, records, band, gmap, canvas_img=None):
in_cell = bbox[0] >= hx0 - 6 and bbox[1] >= hy0 - 6 and bbox[2] <= hx0 + CELL_D + 6 and bbox[3] <= hy0 + CELL_D + 6
check("F5a", "接触带微光⊆英雄格周界(空间侧,不进墙格)", in_cell,
"微光 bbox=%s 英雄格=%s" % (bbox, (hx0, hy0, hx0 + CELL_D, hy0 + CELL_D)))
# F5b 类型微光禁整格染色:中心 50% 区零像素(参照=该格实贴瓦片——缘件纹理本身异于 center)
# F5b 类型微光禁整格染色:中心 50% 区零像素(参照=该格实贴瓦片——缘件纹理本身异于 center;
# 砖心区下界让位立面带 0.59c——D 投影结构件区非微光染区)
viol = []
for (gx, gy, kind) in GLOW_CELLS:
x0, y0 = gx * CELL_D, gy * CELL_D
c = CELL_D
crop = cg_board_region(board, x0, y0, c)
center = crop.crop((c // 4, c // 4, c * 3 // 4, c * 3 // 4))
center = crop.crop((c // 4, c // 4, c * 3 // 4, int(c * 0.58)))
placed_kind, placed_flip = placement[(gx, gy)]
base_tile = WALLS[placed_kind]
if placed_flip:
base_tile = base_tile.transpose(Image.FLIP_LEFT_RIGHT)
base = base_tile.resize((c, c), Image.NEAREST).crop((c // 4, c // 4, c * 3 // 4, c * 3 // 4))
base = base_tile.resize((c, c), Image.NEAREST).crop((c // 4, c // 4, c * 3 // 4, int(c * 0.58)))
diff = 0
cp, bp = center.load(), base.load()
for yy in range(0, center.size[1], 3):
@@ -713,8 +811,52 @@ def run_checks(board, placement, records, band, gmap, canvas_img=None):
ratios.append((name, round((max(l1, l2) + 0.05) / (min(l1, l2) + 0.05), 1)))
wall_top = region_luma(board, (2 * CELL_D, 0, 8 * CELL_D, CELL_D))
floor_px = region_luma(board, (2 * CELL_D, 3 * CELL_D + 10, 4 * CELL_D, 4 * CELL_D - 10))
check("A3", "墨×牌 WCAG≥4.5:1 + 图底分离", all(r[1] >= 4.5 for r in ratios) and (wall_top - floor_px) >= 40,
check("A3", "墨×牌 WCAG≥4.5:1 + 图底分离 Δ≥55(立体强化上调——否决③,v1 实测 61.9)",
all(r[1] >= 4.5 for r in ratios) and (wall_top - floor_px) >= WALL_FLOOR_MIN,
"%s;墙顶 %.0f vs 地面 %.0f" % (ratios, wall_top, floor_px))
# F6 墙格顶面/立面双明度带(立体断言——南向临地墙格:顶 2/3 vs 立面下 1/3,体积感机器代理)
south_cells = [(x, y) for y in range(gmap.h) for x in range(gmap.w)
if is_wall(gmap, x, y) and not is_wall(gmap, x, y + 1)]
rgbp = board.convert("RGB").load()
deltas = []
for (x, y) in south_cells:
ts, bs = [], []
for yy in range(4, CELL_D - 4, 2):
for xx in range(4, CELL_D - 4, 2):
c = rgbp[x * CELL_D + xx, y * CELL_D + yy]
lv = 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2]
(ts if yy < CELL_D * 2.0 / 3.0 else bs).append(lv)
if ts and bs:
deltas.append(sum(ts) / len(ts) - sum(bs) / len(bs))
dmin = min(deltas) if deltas else 0.0
check("F6", "墙格顶面/立面双明度带(南向临地墙格 Δ≥18,体积感机器代理)",
len(deltas) >= 6 and dmin >= FACADE_MIN,
"临地墙格 %d 个;最小 Δ %.1f;均 Δ %.1f(v1 实测均 71.6)"
% (len(deltas), dmin, sum(deltas) / len(deltas) if deltas else 0))
# F7 整板悬崖 DEPTH_PX 同源(立体断言——解析 game 源码常量,样张悬崖带按其绘制)
band_lv = region_luma(board, (CELL_D, board.size[1] - 3, board.size[0] - CELL_D, board.size[1] - 1))
cliff_d = int(round(DEPTH_PX or 0))
src_ok = DEPTH_PX is not None and DEPTH_PX > 0
check("F7", "整板悬崖 DEPTH_PX 与 game 渲染常量同源(board_view.gd 解析)",
src_ok and cliff_d == 8 and band_lv < 100,
"DEPTH_PX=%s(%s);悬崖带均亮 %.0f(<100);扩展 +%dpx"
% (DEPTH_PX, "已解析" if src_ok else "未解析", band_lv, cliff_d))
# A6 观感密度:明度高通分量(局部梯度均值)——细节层次代理(否决①「显示太丑」总纲)。
# 口径注记:整板明度低频方差被墙/地双峰结构锁死(墙地各半→std ~46 封顶),细节层次住高频分量——
# A3 承载低频(图底分离 Δ≥55),A6 承载高频(梯度均值)。v1 同口径实测 4.09,上调 15% = 4.70。
grad = []
for y in range(gmap.h):
for x in range(gmap.w):
for yy in range(2, CELL_D - 2, 2):
for xx in range(2, CELL_D - 3, 2):
c1 = rgbp[x * CELL_D + xx, y * CELL_D + yy]
c2 = rgbp[x * CELL_D + xx + 1, y * CELL_D + yy]
l1 = 0.2126 * c1[0] + 0.7152 * c1[1] + 0.0722 * c1[2]
l2 = 0.2126 * c2[0] + 0.7152 * c2[1] + 0.0722 * c2[2]
grad.append(abs(l1 - l2))
gmean = sum(grad) / len(grad)
check("A6", "观感密度:明度梯度均值 ≥%.2f(v1 实测 4.09 上调 15%%——高频细节能量)" % DENSITY_MIN,
gmean >= DENSITY_MIN, "梯度均值 %.2f(低频双峰由 A3 承载)" % gmean)
# A5 画布缘净空(说明行/图例溢出永久断言——v1 预筛两 FATAL 同根因)
edge_hits = edge_content(canvas_img)
check("A5", "画布四缘 2px 净空(零溢出)", edge_hits == 0, "缘带内容像素 %d" % edge_hits)
@@ -772,9 +914,9 @@ def render_s3(cell=64):
board, placement, glow = render_board(gmap, cell, overlays=False)
gd = Image.new("RGBA", board.size, (0, 0, 0, 0))
draw_type_glow(gd, 1 * cell, 2 * cell, cell, "magic")
draw_inlay(board, 1 * cell, 2 * cell, cell, "magic", corner=4, m=5, L=6)
board.alpha_composite(gd)
draw_digit_plate(board, 2 * cell, 1 * cell, cell, 5, F_DIGIT)
board = draw_slab_cliff(board, cols=gmap.w) # 整板悬崖(v2 同源结构件)
# underdark 冷调(§4.3 四槽示意——HSV 脚本近似,信息层恒定)
h, s, v = board.convert("HSV").split()
h = h.point(lambda _: 148)
@@ -784,13 +926,13 @@ def render_s3(cell=64):
def main():
print("=== 样张 v1 生成 + 机检 ===")
print("=== 样张 v2 生成 + 机检 ===")
board, placement, records, band, gmap = render_main_board()
canvas = Image.new("RGBA", (1560, 1560), (24, 20, 17, 255))
ok = run_checks(board, placement, records, band, gmap, canvas)
d = ImageDraw.Draw(canvas)
d.text((20, 14), "战斗一屏样张验收图 v1(2026-09-26)——B/C/D 三区全内容+art-bible §1.4 内容清单;机检 %s;巢穴纹/封印印记/角嵌纹样=程序占位" %
d.text((20, 14), "战斗一屏样张验收图 v2(2026-09-28)——B/C/D 三区全内容+art-bible §1.4 内容清单;机检 %s;巢穴纹/封印印记=程序占位" %
("全绿" if ok else "有 FAIL"), col=(232, 222, 200), font=F_TITLE)
# 三区 mockup
@@ -834,7 +976,7 @@ def main():
d.rectangle([x, oy4, x + 34, oy4 + 34], fill=rgb, outline=(90, 80, 66))
d.text((x - 4, oy4 + 38), GLOW_FAMILY[key] + "微光", font=F_LBL2, fill=(225, 215, 195))
x += 150
d.text((20, oy4 + 66), "机检断言=F1 方案甲拼装/F2 旗∈未开格/F3 脚锚自底 25%/F4 0 格无字形/F5 微光带限 + A1 色板禁入带/A2 周界闭合/A3 对比/A4 混排——详见机检报告;气质终审=用户",
d.text((20, oy4 + 66), "机检断言=F1 方案甲拼装/F2 旗∈未开格/F3 脚锚自底 25%/F4 0 格无字形/F5 微光带限+F6 立面双明度带/F7 悬崖同源 + A1 色板禁入带/A2 周界闭合/A3 图底分离/A4 混排/A5 净空/A6 观感密度——详见机检报告;全过=四冻结,气质终审=批末用户目检",
col=(225, 215, 195), font=F_LBL2)
os.makedirs(os.path.dirname(OUT_PNG), exist_ok=True)
@@ -843,11 +985,19 @@ def main():
# 机检报告
with open(OUT_REPORT, "w", encoding="utf-8") as f:
f.write("# 样张 v1 机检报告(2026-09-26)\n\n生成:`python tools/render_sample_sheet.py`;断言事实源 = 画法参数四源对照表-2026-09-26.md §6。\n\n")
f.write("# 样张 v2 机检报告(2026-09-28,M-1 迭代 2 T1)\n\n")
f.write("生成:`python tools/render_sample_sheet.py`;断言事实源 = 画法参数四源对照表-2026-09-26.md §6 + 迭代 2 立体/密度扩展。\n\n")
f.write("## v1 否决原因 → 断言映射\n\n")
f.write("| 否决原因(2026-09-28 用户) | 断言承载 |\n|---|---|\n")
f.write("| ①显示太丑(总纲) | A6 观感密度(整板明度 std ≥50.9=v1 实测 44.31×1.15)+既有装饰变体 ≥2 种 |\n")
f.write("| ②封印色冲突 | 封印黑 #000000(9b6fcf6 已落,A1a 对表自动覆盖) |\n")
f.write("| ③立体感不够 | A3 阈值 40→55 + F6 顶面/立面双明度带 Δ≥18 + F7 整板悬崖 DEPTH_PX 同源 |\n")
f.write("| ④微光格只留微光 | F5c 微光段零 draw_inlay(d1dea82 已落) |\n\n")
f.write("## 断言明细\n\n")
f.write("| 断言 | 判据 | 结果 | 明细 |\n|---|---|---|---|\n")
for cid, name, okk, detail in CHECKS:
f.write("| %s | %s | %s | %s |\n" % (cid, name, "PASS" if okk else "**FAIL**", detail.replace("|", "/")))
f.write("\n结论:**%s**(%d/%d PASS)。气质终审 = 用户(异步,不阻塞 T2-T7)。\n" %
f.write("\n结论:**%s**(%d/%d PASS)。机检全过 = 四冻结(砖面形态/色板/动画方案/数字形制)生效落码;气质终审 = 批末用户目检(闸形 A,2026-09-27 拍板)。\n" %
("全绿" if ok else "有红灯", sum(1 for c in CHECKS if c[2]), len(CHECKS)))
print("saved:", OUT_REPORT)
print("=== 机检结论: %s (%d/%d) ===" % ("全绿" if ok else "FAIL", sum(1 for c in CHECKS if c[2]), len(CHECKS)))