# -*- coding: utf-8 -*- """战斗一屏样张方向稿生成脚本 v3-draft(2026-09-29,art-bible v3.0 中修案 B 首批)。 v3.0 口径(方向稿=目检先行闸,冻结归机检批): V1 格底明度分档:已开格底压暗至世界底层档(候选 L 0.08–0.14;§8.5 注记——压暗仅方向稿示意,终稿归地板素材选型/重产) V2 数字亮墨直画:亮族三档(冷亮/暖白/金亮,L≥0.75),色牌降级=复杂格底(物件纹样/装饰变体)后备 V3 装饰物件级解禁演示:小石块堆(专属装饰形状家族,圆钝三角形,零信息纪律不变) V4 B 区舞台正面目标:世界底层暗背景+顶光柱+地台落影(§6.1 地窟看台) A3 断言改造:亮墨×实测已开格底 WCAG ≥4.5:1(色牌后备组维持深墨×牌 ≥4.5:1) 基于 v2 脚本定点补丁生成(_v3_patch.py),v2 冻结产物管线不动。 v1 用户否决四原因(2026-09-28)全部断言化——生成即验,不过闸不出图: F1 未开格 = 方案甲缘件 8 邻拼装(词表查表,禁整块 center 铺满) F2 旗仅出现于未开格 F3 小人脚锚 = 自底边 25% 线(⚠ 纵向比例一律自底边口径) F4 数字 0 格无字形(纯安全格 = 干净裸地面) F5 微光仅带状:接触带微光只画英雄格周界空间侧;类型微光禁整格染色;微光格零附加图形(角嵌退役) F6 墙格顶面/立面双明度带(否决③立体感——南向临地墙格 Δ≥18,体积感机器代理) F7 整板悬崖 DEPTH_PX 与 game 渲染常量同源(board_view.gd 解析——否决③立体感) 程序化审计(§9 初级验收授权 + 迭代 2 扩展): A1 色板对表圣经冻结色板 + 禁入带(HSV 300°-345°)全图扫描 A2 拼缝周界闭合(墙↔地边界墙侧 2px 墨线带亮度 <70) 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;画法参数事实源 = design/research/画法参数四源对照表-2026-09-26.md(四源对表先行产物)。 输出 = artwork/mockups/sample-batch-v3-final-2026-09-30.png(v3.5 冻结批产物转正——2026-09-30 批 A) + 机检报告(中间产物,吸收即死不入 git;断言定值=A3b Δ≥30/A4 尺寸基线)。 用法:python tools/render_sample_sheet_v3.py """ from PIL import Image, ImageDraw, ImageFilter, ImageChops, ImageFont import os import sys import math import re import hashlib from collections import deque ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) MAIN_ROOT = "D:/code/g-plan2" def asset_root(*parts): """素材路径两级回退:worktree → 主检出(素材不入 git,fresh worktree 缺位)。""" for base in (ROOT, MAIN_ROOT): p = os.path.join(base, *parts) if os.path.exists(p): return p raise FileNotFoundError(os.path.join(*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-v3-final-2026-09-30.png") OUT_REPORT = os.path.join(ROOT, "design", "research", "sample-v3-final-机检报告-2026-09-30.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" # ══ 冻结色板(art-bible §4.1——A1 逐字节对表基准,禁改动) ══ INK = (51, 38, 27) # 主墨 #33261B INK_COLD = (19, 46, 54) # 青墨 1-3 #132E36 INK_OCHRE = (60, 37, 12) # 赭墨 4-5 #3C250C INK_RUST = (74, 30, 20) # 锈墨 6-8 #4A1E14 PLATE = (245, 234, 211) # 数字色牌 #F5EAD3(v3.0 后备件) # ── v3 亮族数字墨(art-bible v3.0:直画已开格底世界底层档,候选 L≥0.75;金亮低饱和避奖励金语义) ── INK_L_COLD = (216, 236, 240) # 冷亮青白 1-3 #D8ECF0 INK_L_WARM = (240, 226, 200) # 暖白 4-5 #F0E2C8(关卡屏 WARM 同族) INK_L_GOLD = (248, 227, 168) # 金亮 6-8 #F8E3A8 FLOOR_DARK_GAMMA = 1.75 # 已开格底压暗曲线(方向稿示意):gamma 保纹理相对对比 FLOOR_DARK_GAIN = 0.92 SEAL_BLACK = (0, 0, 0) # 封印黑 #000000(旗朱→封印紫→封印黑 2026-09-28——紫与微光蓝族色相冲突退役) DANGER_RED = (168, 50, 31) # 危险红 #A8321F REWARD_GOLD = (240, 168, 40) # 奖励金 #F0A828 HERO_BLUE = (46, 134, 222) # 小人蓝 #2E86DE(候选,样张批冻结——对照表 §5.1) RED_MINE = (150, 44, 36) # 引爆雷字朱(游戏 UiKit.RED_MINE) # 候选(样张批冻结项——对照表 §5) CONTACT_GLOW = (214, 236, 255) # 接触带微光:小人蓝族浅两档(近白蓝) GLOW_GROUPS = { # §4.2 类型微光五组候选 "attack": (214, 193, 58), "heal": (92, 184, 58), "pierce": (46, 158, 158), "magic": (123, 91, 214), "blast": (255, 45, 85), "gp": (214, 193, 58), } GLOW_FAMILY = {"attack": "通用增益", "defense": "通用增益", "weaken": "通用增益", "action": "通用增益", "gp": "通用增益", "heal": "治疗", "pierce": "穿刺", "magic": "魔法", "blast": "群伤"} # 游戏 UiKit 常量(A4 混排对表——ui_kit.gd 实测值) UIKIT = {"INK": INK, "FLAG_RED": SEAL_BLACK, "GOLD": REWARD_GOLD, "RED_MINE": RED_MINE, "PANEL_STAGE": (216, 200, 172), "PANEL_HERO": (232, 220, 196), "PANEL_DATA": (226, 210, 182), "PANEL_BORDER": (120, 106, 88), "BRICK": (201, 175, 131), "FLOOR": (179, 154, 120)} ARMOR_ICONS = {"light": "◇", "ward": "◆", "heavy": "▣"} ARMOR_COLORS = {"light": (120, 110, 98), "ward": HERO_BLUE, "heavy": (176, 122, 20)} F_DIGIT = ImageFont.truetype(T_FONT, 30) # S2 64px 格数字 F_DIGIT_D = ImageFont.truetype(T_FONT, 18) # D 区 45px 格数字(下限 ≥18px) F_SMALL = ImageFont.truetype(T_FONT, 16) F_LBL = ImageFont.truetype(ZH_FONT, 14) F_LBL2 = ImageFont.truetype(ZH_FONT, 12) F_TITLE = ImageFont.truetype(ZH_FONT, 19) F_STANDEE = ImageFont.truetype(ZH_FONT, 42) # 敌像字形占位 F_NAME = ImageFont.truetype(ZH_FONT, 12) F_ORDER = ImageFont.truetype(ZH_FONT, 13) F_INTENT = ImageFont.truetype(ZH_FONT, 15) CHECKS = [] # (id, name, ok, detail) def check(cid, name, ok, detail=""): CHECKS.append((cid, name, bool(ok), detail)) print("[%s] %s — %s%s" % ("PASS" if ok else "FAIL", cid, name, (";" + detail) if detail else "")) # ══ 素材装载 ══ def load_tile(name): return Image.open(asset_root(*(BOARD_DIR + (name,)))).convert("RGB") WALLS = {} for k in ["center", "center_crack", "center_speckle", "n", "s", "w", "nw", "sw", "hrun", "vrun", "end_n", "end_s", "end_w", "lone", "in_nw", "in_sw"]: WALLS[k] = load_tile("terrain_wall_%s_dungeon.png" % k) FLOORS = [load_tile("terrain_floor_dungeon_%d.png" % i) for i in range(1, 6)] def _v3_darken_floors(tiles, target_l=0.10): """v3 方向稿:已开格底归一到世界底层档中点 L≈0.11(候选带 0.08–0.14)——实测现行瓦片 L≈0.036 低于带下沿,故实为**提亮**;线性增益保纹理相对对比。终稿归素材重产(§8.5),本函数仅方向稿示意。""" def _lum(rgb): def lin(c): c = c / 255.0 return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4 r, g, b = rgb[:3] return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b) out = [] for tl in tiles: rgb = tl.convert("RGB") probe = list(rgb.resize((8, 8)).getdata()) cur = sum(_lum(c) for c in probe) / len(probe) k = (target_l / max(cur, 1e-3)) ** (1 / 2.2) # L ∝ c^2.2 近似 k = max(0.6, min(1.8, k)) lut = [min(255, int(i * k + 0.5)) for i in range(256)] a = tl.split()[3] if tl.mode == "RGBA" else None rgb = rgb.point(lut * 3) if a is not None: rgb.putalpha(a) out.append(rgb.convert("RGBA")) print("[v3] floor normalize L %.3f → %.3f (k=%.2f)" % (cur, target_l, k)) return out FLOORS = _v3_darken_floors(FLOORS) HERO_SRC = Image.open(asset_root(*(CHAR_DIR + ("character_hero_idle.png",)))).convert("RGBA") PORTRAIT_SRC = Image.open(asset_root(*(CHAR_DIR + ("character_hero_portrait.png",)))).convert("RGBA") STATUS_ICONS = {k: Image.open(asset_root(*(ICON_DIR + ("status_%s.png" % k,)))).convert("RGBA") for k in ["attack_up", "ac_up", "alert"]} def hero_sprite_transparent(src): """素材黑幕去底:四角泛洪把连通近黑背景转透明(角色内部深色像素不受影响)。""" im = src.copy() w, h = im.size px = im.load() seen = [[False] * w for _ in range(h)] q = deque() for sx, sy in [(0, 0), (w - 1, 0), (0, h - 1), (w - 1, h - 1)]: q.append((sx, sy)) while q: x, y = q.popleft() if x < 0 or y < 0 or x >= w or y >= h or seen[y][x]: continue seen[y][x] = True r, g, b, a = px[x, y] if r < 14 and g < 14 and b < 14: px[x, y] = (r, g, b, 0) q.extend([(x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)]) return im HERO = hero_sprite_transparent(HERO_SRC) # 裁除透明留白(v1 预筛发现:素材底部 7 行全透明——直接缩放则视觉脚浮于锚线上方 ~10px。 # 裁后按内容 bbox 缩放,视觉脚底 = 脚锚线。游戏内 BoardWorld.ensure_hero 用原素材同病 # (视觉脚浮 ~10px)——呈现缺陷登记移交,本样张按设计规则出图) _HB = HERO.getbbox() HERO = HERO.crop(_HB) # ══ 方案甲词表查表(board-tile-vocab-plan-a §三 逐条转写) ══ def is_wall(gmap, x, y): if x < 0 or y < 0 or x >= gmap.w or y >= gmap.h: return True # 板外视为墙(词表 §三.1) return gmap.grid[y][x] == "W" def lookup_wall_tile(gmap, x, y): """四正交邻 → 16 情形;四邻皆墙查对角(凹角回落优先级 nw>ne>sw>se);center 变体哈希 60/20/20。""" n, e, s, w = (is_wall(gmap, x, y - 1), is_wall(gmap, x + 1, y), is_wall(gmap, x, y + 1), is_wall(gmap, x - 1, y)) flip = False if n and e and s and w: dnw, dne, dsw, dse = ((not is_wall(gmap, x - 1, y - 1)), (not is_wall(gmap, x + 1, y - 1)), (not is_wall(gmap, x - 1, y + 1)), (not is_wall(gmap, x + 1, y + 1))) if dnw: return "in_nw", False if dne: return "in_nw", True # in_ne ← in_nw 镜像 if dsw: return "in_sw", False if dse: return "in_sw", True # in_se ← in_sw 镜像 h = int(hashlib.md5(("wall%d_%d" % (x, y)).encode()).hexdigest(), 16) % 100 if h < 60: return "center", False if h < 80: return "center_crack", False return "center_speckle", False if not n and s and e and w: return "n", False if not s and n and e and w: return "s", False if not w and n and e and s: return "w", False if not e and n and s and w: return "w", True # wall_e ← wall_w 镜像 if not n and not s and e and w: return "hrun", False if not w and not e and n and s: return "vrun", False if not n and not w and s and e: return "nw", False if not n and not e and s and w: return "nw", True # wall_ne ← wall_nw 镜像 if not s and not w and n and e: return "sw", False if not s and not e and n and w: return "sw", True # wall_se ← wall_sw 镜像 if not n and not e and not w and s: return "end_n", False if not s and not e and not w and n: return "end_s", False if not w and not n and not s and e: return "end_w", False if not e and not n and not s and w: return "end_w", True # wall_end_e ← wall_end_w 镜像 return "lone", False class GMap(object): def __init__(self, rows): self.grid = rows self.w = len(rows[0]) self.h = len(rows) def floor_variant(x, y): """地面 5 变体按格坐标哈希(词表 §三.3——初始化随机落定)。""" 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", "WWWWWWWWWW", "WWFFWWFFWF", "WWFFFFWFWF", "WWFFFFWFWF", "WWFFFFWFWF", "WFFFFFFWFF", "FFFFFFWWFF", "FFWWWWWWFF", "FFFFFWWWFF", ] # 覆盖层布置(格坐标) CRACK_AT = (3, 0) # center 裂纹变体 SPECKLE_AT = (6, 0) # center 碎石斑变体 FLAG_AT = (1, 2) # 封印印记——未开格(F2) HOVER_AT = (1, 4) # 悬停金框(预承诺光标态) HERO_AT = (2, 4) # 小人独立占格 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", (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) 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): c = c / 255.0 return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4 r, g, b = rgb[:3] return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b) def draw_digit_plate(img, x0, y0, cell, digit, font, plate_size=None, ink=None): """数字色牌:素色直角小牌+墨档数字(§4.1)。返回牌矩形。""" d = ImageDraw.Draw(img) pw, ph = plate_size or (int(cell * 0.60), int(cell * 0.62)) px0 = x0 + (cell - pw) // 2 py0 = y0 + (cell - ph) // 2 d.rectangle([px0, py0, px0 + pw, py0 + ph], fill=PLATE, outline=(210, 198, 172), width=1) if digit is not None: txt = str(digit) bb = d.textbbox((0, 0), txt, font=font) tw, th = bb[2] - bb[0], bb[3] - bb[1] d.text((px0 + (pw - tw) // 2 - bb[0], py0 + (ph - th) // 2 - bb[1]), txt, font=font, fill=ink or digit_ink_deep(digit)) return (px0, py0, pw, ph) def draw_digit_direct(img, x0, y0, cell, digit, font): """v3 亮墨直画(无牌,数字禁描边——§4.1 字形唯一数值载体)。""" d = ImageDraw.Draw(img) txt = str(digit) bb = d.textbbox((0, 0), txt, font=font) tw, th = bb[2] - bb[0], bb[3] - bb[1] d.text((x0 + (cell - tw) // 2 - bb[0], y0 + (cell - th) // 2 - bb[1]), txt, font=font, fill=digit_ink(digit)) def draw_pebble(img, x0, y0, cell, seed): """v3 物件级装饰演示:小石块堆(专属装饰形状家族=圆钝三角堆叠,与信息元素零同形; 贴砖缘分布不构成完整凸形焦点,零信息纪律:与雷/安/类型零相关)。""" rnd = __import__("random").Random(seed) d = ImageDraw.Draw(img) bx, by = x0 + int(cell * 0.72), y0 + int(cell * 0.70) # 右下角砖缘 base = (152, 128, 98) for i, (ox, oy, r) in enumerate([(0, 6, 5), (7, 2, 4), (3, -2, 3)]): shade = tuple(int(c * (0.9 + 0.08 * i)) for c in base) d.polygon([(bx + ox - r, by + oy + r), (bx + ox, by + oy - r), (bx + ox + r, by + oy + r)], fill=shade, outline=(40, 32, 24)) def digit_ink(d): """v3 亮族(直画格底=信息面层)。""" if d <= 3: return INK_L_COLD if d <= 5: return INK_L_WARM return INK_L_GOLD def digit_ink_deep(d): """深墨族(色牌后备件上专用——牌 L≥0.70 配深墨)。""" if d <= 3: return INK_COLD if d <= 5: return INK_OCHRE return INK_RUST def draw_inlay(img, ox, oy, cell, kind, corner=4, m=None, L=None): """角嵌纹样(墨族单色,贴砖缘/砖角,砖心留白——§3.2;已开格=物件纹样同语言)。程序化示意。""" layer = Image.new("RGBA", img.size, (0, 0, 0, 0)) d = ImageDraw.Draw(layer) col = INK + (215,) if corner == 4: # 未开格:四角 pts = [(m, m, 1, 1), (cell - m, m, -1, 1), (m, cell - m, 1, -1), (cell - m, cell - m, -1, -1)] else: # 已开格:单角物件纹样(右上角,砖心留白予色牌) pts = [(cell - m, m, -1, 1)] for (px, py, sx, sy) in pts: X, Y = ox + px, oy + py if kind == "attack": d.line([(X, Y), (X + sx * L, Y + sy * L)], fill=col, width=2) d.line([(X + sx * 2, Y), (X, Y + sy * 2)], fill=col, width=1) elif kind == "defense": d.line([(X, Y + sy * L), (X, Y), (X + sx * L, Y)], fill=col, width=2) elif kind == "heal": r = 2 d.ellipse([X - r, Y - r, X + r, Y + r], fill=col) elif kind == "pierce": d.polygon([(X, Y), (X + sx * L, Y + sy * 2), (X + sx * 2, Y + sy * L)], outline=col, width=1) elif kind == "magic": d.line([(X - sx * 4, Y), (X + sx * 4, Y)], fill=col, width=1) d.line([(X, Y - sy * 4), (X, Y + sy * 4)], fill=col, width=1) elif kind == "blast": d.line([(X, Y), (X + sx * L, Y)], fill=col, width=1) d.line([(X, Y), (X, Y + sy * L)], fill=col, width=1) d.line([(X, Y), (X + sx * L, Y + sy * L)], fill=col, width=1) elif kind == "gp": r = 3 d.ellipse([X - r, Y - r, X + r, Y + r], outline=col, width=1) d.line([(X, Y - r - 2), (X, Y - r)], fill=col, width=1) img.alpha_composite(layer) def draw_type_glow(glow_layer, ox, oy, cell, kind): """类型常显微光=内缘带状泛光(§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(4, cell // 8) d.rounded_rectangle([ox + 2, oy + 2, ox + cell - 3, oy + cell - 3], radius=5, outline=rgb + (170,), width=band) layer = layer.filter(ImageFilter.GaussianBlur(1)) glow_layer.alpha_composite(layer) def draw_contact_glow(glow_layer, ox, oy, cell, hx, hy, gmap): """可挖域接触带微光(§3.2):英雄格周界空间侧——正交邻墙=整条边,斜向=共享角点+两侧各半条边。 返回带几何(机检 F5a 用)。""" layer = Image.new("RGBA", glow_layer.size, (0, 0, 0, 0)) d = ImageDraw.Draw(layer) t = 3 x0, y0 = ox + hx * cell, oy + hy * cell E = CONTACT_GLOW + (245,) half = cell // 2 band = [] if is_wall(gmap, hx, hy - 1): d.line([(x0 + 2, y0 + t), (x0 + cell - 2, y0 + t)], fill=E, width=5) band.append(("edge", (x0, y0, x0 + cell, y0 + 2 * t + 2))) if is_wall(gmap, hx, hy + 1): d.line([(x0 + 2, y0 + cell - t), (x0 + cell - 2, y0 + cell - t)], fill=E, width=5) band.append(("edge", (x0, y0 + cell - 2 * t - 2, x0 + cell, y0 + cell))) if is_wall(gmap, hx - 1, hy): d.line([(x0 + t, y0 + 2), (x0 + t, y0 + cell - 2)], fill=E, width=5) band.append(("edge", (x0, y0, x0 + 2 * t + 2, y0 + cell))) if is_wall(gmap, hx + 1, hy): d.line([(x0 + cell - t, y0 + 2), (x0 + cell - t, y0 + cell - 2)], fill=E, width=5) band.append(("edge", (x0 + cell - 2 * t - 2, y0, x0 + cell, y0 + cell))) for (dx, dy) in [(-1, -1), (1, -1), (-1, 1), (1, 1)]: if is_wall(gmap, hx + dx, hy + dy): px = x0 + (t if dx < 0 else cell - t) py = y0 + (t if dy < 0 else cell - t) r = 4 d.ellipse([px - r, py - r, px + r, py + r], fill=(255, 255, 255, 255)) d.line([(px, py), (px - dx * half, py)], fill=E, width=4) d.line([(px, py), (px, py - dy * half)], fill=E, width=4) band.append(("corner", (px - r, py - r, px + r, py + r))) band.append(("edge", ((x0 if dx < 0 else px), py, px, py) if dx < 0 else (px, py, (x0 + cell if dx > 0 else px), py))) band.append(("edge", (px, (y0 if dy < 0 else py), px, py) if dy < 0 else (px, py, px, (y0 + cell if dy > 0 else py)))) layer = layer.filter(ImageFilter.GaussianBlur(1)) glow_layer.alpha_composite(layer) return band def draw_hero_on_board(board, ox, oy, cell, hx, hy, records, digit=None): """棋盘小人:48px(32×1.5),脚锚=格底往上 25% 线,椭圆影贴脚(F3 断言记录进 records)。""" fx = ox + hx * cell + cell // 2 fy = oy + (hy + 1) * cell - cell * GROUND_FRAC _sc = HERO_PX / HERO.size[1] sp = HERO.resize((max(1, int(HERO.size[0] * _sc)), HERO_PX), Image.NEAREST) sh = Image.new("RGBA", (int(HERO_PX * 32 / 24), 16), (0, 0, 0, 0)) # 32×12@1× → ×1.5=48×18?按 HERO_SHADOW_PX 32,12 ×1.5 档 sh = Image.new("RGBA", (48, 18), (0, 0, 0, 0)) ds = ImageDraw.Draw(sh) ds.ellipse([2, 3, 46, 15], fill=(0, 0, 0, 90)) board.alpha_composite(sh, (int(fx - 24), int(fy - 9))) board.alpha_composite(sp, (int(fx - sp.size[0] // 2), int(fy - HERO_PX))) d = ImageDraw.Draw(board) d.rectangle([ox + hx * cell + 1, oy + hy * cell + 1, ox + hx * cell + cell - 2, oy + hy * cell + cell - 2], outline=HERO_BLUE, width=3) if digit is not None: # v3 亮墨直画退避左上角(§1.2 优先级栈:数字>小人标记;蓝框不覆盖文本,§1.4) d2 = ImageDraw.Draw(board) txt = str(digit) bb = d2.textbbox((0, 0), txt, font=F_DIGIT_D) d2.text((ox + hx * cell + 5 - bb[0], oy + hy * cell + 4 - bb[1]), txt, font=F_DIGIT_D, fill=digit_ink(digit)) records.append(("hero", hx, hy, fx, fy, digit)) FACADE_FRAC = 0.30 # E 格墙面高度(新 v3.5:用户指令 30%) def draw_facade(board, x0, y0, cell): """v3.3 E 格墙面(用户方案:地面格南邻墙 → 南部承接墙面展示)。 墙面=墙顶同款岩块材质(center 瓦片纹理直接贴,明度 ×0.85 表达垂直面),禁平涂新颜色: 北端=墙脚阴线(与地面交界),南端=檐口亮棱(与墙顶交界,受光棱线)。""" d = ImageDraw.Draw(board) fy0 = y0 + int(cell * (1.0 - FACADE_FRAC)) wall_top = WALLS["center"].resize((cell, cell), Image.NEAREST) face = wall_top.crop((0, 0, cell, cell - fy0 + y0)) # 新 v3.5 ①:(b) 与 (c) 墙顶同色——顶面材质原亮度直贴,零暗档 board.paste(face, (x0, fy0)) d.rectangle([x0, fy0, x0 + cell, fy0 + 1], fill=(52, 42, 32)) # 墙脚阴线(地面/墙面交界) # v3.4:墙面底缘不画任何线——墙面直接接到南侧墙顶(用户裁:白边导致两截) def is_facade_cell(gmap, x, y): """E 格判定:本格是地面,且南邻(y+1)是墙。连续墙体内部不出立面(立面只露在地格侧)。""" return (0 <= y + 1 < gmap.h) and gmap.grid[y][x] != "W" and gmap.grid[y + 1][x] == "W" def lift_south_lip(tile): """v3.3:南向墙格(南邻地)底部暗条改为顶面同款材质轻微暗档(×0.80)—— 同一堵墙的顶面材质全板一致,南向厚度面只差明度,不再两截皮。终稿归瓦片重产(§8.5)。""" im = tile.convert("RGB") w, h = im.size y0 = int(h * 0.60) top_ref = im.crop((0, 0, w, y0)) # 顶面区材质 band_h = h - y0 ref = top_ref.resize((w, band_h)) # 顶面纹理拉伸到暗条区(同材质) ref = ref.point([min(255, int(i * 0.80)) for i in range(256)] * 3) out = im.copy() out.paste(ref, (0, y0)) if tile.mode == "RGBA": out.putalpha(tile.split()[3]) return out.convert("RGBA") def render_board(gmap, cell, overlays=None, scale_note=""): """按方案甲查表拼装渲染样张板。返回 (img, placement, glow_layer)。""" wpx, hpx = gmap.w * cell, gmap.h * cell board = Image.new("RGBA", (wpx, hpx), (0, 0, 0, 255)) placement = {} for y in range(gmap.h): for x in range(gmap.w): if gmap.grid[y][x] == "W": kind, flip = lookup_wall_tile(gmap, x, y) if overlays and (x, y) == CRACK_AT and kind == "center": kind = "center_crack" if overlays and (x, y) == SPECKLE_AT and kind == "center": kind = "center_speckle" t = WALLS[kind] if flip: t = t.transpose(Image.FLIP_LEFT_RIGHT) t = t.resize((cell, cell), Image.NEAREST) if overlays and not is_wall(gmap, x, y + 1): # v3.2 南向墙格立面提亮 t = lift_south_lip(t) board.paste(t, (x * cell, y * cell)) placement[(x, y)] = (kind, flip) else: vi = floor_variant(x, y) if overlays and (x, y) == VARIANT_FLOOR_AT: 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: for y in range(gmap.h): # v3.1 E 格立面(高墙双格呈现——结构件,先于投影) for x in range(gmap.w): if is_facade_cell(gmap, x, y): draw_facade(board, x * cell, y * cell, cell) board = apply_d_projection(board, gmap, cell) # D 投影档(立体承载) glow = Image.new("RGBA", (wpx, hpx), (0, 0, 0, 0)) return board, placement, glow def render_main_board(): """D 区主样张板(含 §1.4 全部内容组)。返回 (board, placement, records)。""" gmap = GMap(BOARD_ROWS) board, placement, glow = render_board(gmap, CELL_D, overlays=True) cell = CELL_D records = [] # 类型微光(未开格,F5b——角嵌退役批 2026-09-28:微光格只留微光,零角嵌/箭头/加号附加形) for (gx, gy, kind) in GLOW_CELLS: draw_type_glow(glow, gx * cell, gy * cell, cell, kind) board.alpha_composite(glow) # 数字格(已开;v3 亮墨直画,复杂格底(物件纹样/装饰变体)垫色牌后备——v3.0 原则二;0 格 = 纯地面无字形——F4) for (dx, dy), dg in sorted(DIGITS.items()): complex_base = False if (dx, dy) in INLAY_RESULT: draw_inlay(board, dx * cell, dy * cell, cell, INLAY_RESULT[(dx, dy)], corner=1, m=6, L=8) # 物件纹样右上角,砖心留白 complex_base = True if (dx, dy) == VARIANT_FLOOR_AT: complex_base = True if dg > 0: if complex_base: draw_digit_plate(board, dx * cell, dy * cell, cell, dg, F_DIGIT_D) # 色牌后备组 else: draw_digit_direct(board, dx * cell, dy * cell, cell, dg, F_DIGIT_D) # 亮墨直画组 # 物件级装饰演示(v3 解禁——未开格砖缘小石块堆,专属形状家族,零信息) draw_pebble(board, 8 * cell, 0, cell, seed=11) draw_pebble(board, 0, 6 * cell and 6 * cell, cell, seed=23) if False else None draw_pebble(board, 6 * cell, 1 * cell, cell, seed=23) # 封印旗(未开格,F2) fx0, fy0 = FLAG_AT[0] * cell, FLAG_AT[1] * cell d = ImageDraw.Draw(board) px, py = fx0 + cell // 2 - 2, fy0 + 8 d.line([(px, py), (px, fy0 + cell - 10)], fill=(222, 210, 186), width=3) d.polygon([(px, py), (px + 18, py + 6), (px, py + 12)], fill=SEAL_BLACK) # 旗面恒画右 d.ellipse([px - 3, fy0 + cell - 12, px + 3, fy0 + cell - 6], fill=INK) # 危险格巢穴(已开,程序占位——素材缺口,画面内标注) nx0, ny0 = NEST_AT[0] * cell, NEST_AT[1] * cell nest_glow = Image.new("RGBA", board.size, (0, 0, 0, 0)) dg2 = ImageDraw.Draw(nest_glow) dg2.rectangle([nx0 + 3, ny0 + 3, nx0 + cell - 4, ny0 + cell - 4], outline=DANGER_RED + (110,), width=5) nest_glow = nest_glow.filter(ImageFilter.GaussianBlur(2)) board.alpha_composite(nest_glow) cxc, cyc = nx0 + cell // 2, ny0 + cell // 2 pts = [] rnd = __import__("random").Random(7) for i in range(14): ang = 6.283 * i / 14 r = 11 + (4 if i % 2 else 0) + rnd.randint(-2, 2) pts.append((cxc + r * 1.2 * math.cos(ang), cyc + r * 0.85 * math.sin(ang))) d.polygon(pts, fill=(24, 16, 12), outline=DANGER_RED) for i in range(6): ang = 6.283 * i / 6 + 0.4 d.line([(cxc + 9 * math.cos(ang), cyc + 7 * math.sin(ang)), (cxc + 15 * math.cos(ang), cyc + 12 * math.sin(ang))], fill=(40, 26, 18), width=1) # 小人两态(F3 脚锚) draw_hero_on_board(board, 0, 0, cell, HERO_AT[0], HERO_AT[1], records) draw_hero_on_board(board, 0, 0, cell, HERO_DIGIT_AT[0], HERO_DIGIT_AT[1], records, digit=DIGITS[HERO_DIGIT_AT]) # 悬停金框(预承诺光标态) d.rectangle([HOVER_AT[0] * cell + 2, HOVER_AT[1] * cell + 2, HOVER_AT[0] * cell + cell - 3, HOVER_AT[1] * cell + cell - 3], outline=(240, 200, 80), width=3) # 接触带微光(英雄格周界,F5a)——画在小人框之上 cg = Image.new("RGBA", board.size, (0, 0, 0, 0)) band = draw_contact_glow(cg, 0, 0, cell, HERO_AT[0], HERO_AT[1], gmap) board.alpha_composite(cg) # 网格线(屏轴对齐,低对比);新 v3.5 ③:(b)-(c) 连接处(上格=地面且南邻墙/下格=墙)无边框 d = ImageDraw.Draw(board) for i in range(gmap.w + 1): d.line([(i * cell, 0), (i * cell, gmap.h * cell)], fill=(0, 0, 0, 45), width=1) for i in range(1, gmap.h + 1): x = 0 bottom_edge = (i == gmap.h) while x < gmap.w: skip = (not bottom_edge) and (gmap.grid[i][x] == "W" and gmap.grid[i - 1][x] != "W") x2 = x while x2 < gmap.w and ((not bottom_edge) and (gmap.grid[i][x2] == "W" and gmap.grid[i - 1][x2] != "W")) == skip: x2 += 1 if not skip: d.line([(x * cell, i * cell), (x2 * cell, i * cell)], fill=(0, 0, 0, 45), width=1) x = x2 # 整板悬崖(v2 立体强化——board_world 同款结构件,F7 同源) board = draw_slab_cliff(board, cols=gmap.w) return board, placement, records, band, gmap # ══ B 区站桩(mockup 1:1) ══ def draw_standee(canvas, ox, oy, kind, name, enemy_id, order=1, armor="light", hp=1.0, intent="", statuses=(), dead=False): """站桩 150×176(standee_view.gd 几何):血条+状态行头顶/立绘盒 96px/底座/敌名行。""" d = ImageDraw.Draw(canvas) W, H = 150, 176 # 头顶:顺位徽标(敌)+血条 tx = ox + 8 if kind == "enemy": d.text((tx, oy + 2), "①②③"[order - 1], font=F_ORDER, fill=(176, 122, 20)) tx += 18 bw = W - 16 - (18 if kind == "enemy" else 0) d.rectangle([tx, oy + 4, tx + bw, oy + 14], fill=(60, 52, 42)) d.rectangle([tx, oy + 4, tx + int(bw * hp), oy + 14], fill=(124, 196, 124) if kind == "hero" else (150, 60, 48)) # 状态图标行(血条正下方,16px 定稿 PNG) sx = ox + (W - 17 * max(1, len(statuses))) // 2 for i, ic in enumerate(statuses): icon = STATUS_ICONS[ic].resize((16, 16), Image.NEAREST) canvas.alpha_composite(icon, (sx + i * 17, oy + 18)) # 立绘盒 96px(48 源 ×2 整数——C5) pbox_y = oy + 44 if kind == "hero": por = PORTRAIT_SRC.resize((96, 96), Image.NEAREST) canvas.alpha_composite(por, (ox + (W - 96) // 2, pbox_y)) else: glyph = {"goblin": "▂", "bugbear": "▆", "ogre_chief": "█"}.get(enemy_id, "敌") col = (150, 60, 48) if not dead else (110, 104, 98) d.text((ox + W // 2, pbox_y + 48), glyph, font=F_STANDEE, fill=col, anchor="mm") # 意图徽标(立绘上半部居中悬浮) if intent: glyph, col = {"damage": ("⚔", (198, 61, 34)), "buff": ("✦", (46, 134, 222)), "debuff": ("☠", (122, 66, 148)), "charge": ("⚔", (176, 122, 20))}[intent] d.text((ox + W // 2, pbox_y + 30), glyph, font=F_INTENT, fill=col, anchor="mm") # v3 地台+落影(§6.1 舞台正面目标:石板地台承载,椭圆落影压脚) sh = Image.new("RGBA", (W, 30), (0, 0, 0, 0)) ImageDraw.Draw(sh).ellipse([28, 6, W - 28, 24], fill=(0, 0, 0, 70)) canvas.alpha_composite(sh, (ox, oy + 146)) d.rectangle([ox + 18, oy + 158, ox + W - 18, oy + 170], fill=(150, 130, 100), outline=(90, 76, 58)) d.rectangle([ox + 18, oy + 158, ox + W - 18, oy + 160], fill=(196, 172, 132)) # 敌名行:护甲类型图标+纯名字(2026-09-26 终裁——AC 数值不展示) if kind == "enemy": d.text((ox + 16, oy + 172), ARMOR_ICONS[armor], font=F_NAME, fill=ARMOR_COLORS[armor], anchor="lm") d.text((ox + 30, oy + 172), name, font=F_NAME, fill=(240, 226, 200), anchor="lm") if dead: gray = Image.new("RGBA", (W, H), (100, 100, 100, 120)) canvas.alpha_composite(gray, (ox, oy)) def render_battle_screen(): """B/C/D 三区全内容 1280×800 mockup。""" scr = Image.new("RGBA", (1280, 800), (24, 20, 17, 255)) d = ImageDraw.Draw(scr) # ── B 区 252px 全宽(v3 舞台:世界底层暗背景+顶光柱氛围;文字=信息面层暖白) ── d.rectangle([0, 0, 1280, 252], fill=(62, 52, 42), outline=UIKIT["PANEL_BORDER"]) beam = Image.new("RGBA", (1280, 252), (0, 0, 0, 0)) db = ImageDraw.Draw(beam) for bx0 in (110, 900, 1055, 1210): for i in range(60): a = int(26 * (1 - i / 60.0)) db.rectangle([bx0 - 46 + i // 3, 0, bx0 + 46 - i // 3, 40 + i * 3], fill=(255, 236, 200, a)) scr.alpha_composite(beam, (0, 0)) d.text((640, 14), "第 3 轮 · 挖掘", font=ImageFont.truetype(ZH_FONT, 15), fill=(240, 226, 200), anchor="mm") draw_standee(scr, 40, 34, "hero", "主角", "", hp=0.7, statuses=("attack_up", "ac_up")) d.text((560, 120), "兑现演出区\n(结算期:光带 → 弹道 → 受击)", font=ImageFont.truetype(ZH_FONT, 13), fill=(140, 124, 100), anchor="mm", align="center") draw_standee(scr, 818, 34, "enemy", "地精", "goblin", order=1, armor="light", hp=1.0) draw_standee(scr, 973, 34, "enemy", "熊地精", "bugbear", order=2, armor="light", hp=0.8, intent="charge", statuses=("alert",)) draw_standee(scr, 1128, 34, "enemy", "食人魔酋长", "ogre_chief", order=3, armor="light", hp=1.0, intent="buff") # 明细带(挖掘期收起 24px) d.rectangle([0, 228, 1280, 252], fill=(46, 39, 32)) d.text((10, 240), "── 第 3 轮 ── 地精 受到 4 点伤害(增伤 ×1)· 熊地精 蓄力(重击预告)…", font=F_LBL2, fill=(240, 226, 200), anchor="lm") # ── C 区 240×548 ── d.rectangle([0, 256, 240, 800], fill=UIKIT["PANEL_HERO"], outline=UIKIT["PANEL_BORDER"]) d.rectangle([8, 264, 232, 340], outline=(120, 106, 88), width=1) # 攻击数据框(直角细线) d.text((16, 276), "攻击 6", font=ImageFont.truetype(ZH_FONT, 15), fill=(30, 26, 22)) d.text((16, 298), "(+50% 增伤 ×1)", font=F_LBL2, fill=(140, 90, 30)) d.text((16, 318), "护甲 AC 12", font=ImageFont.truetype(ZH_FONT, 15), fill=(30, 26, 22)) d.text((8, 352), "魔法道具槽(预留)", font=F_LBL2, fill=(120, 110, 98)) for i in range(4): d.rectangle([8 + i * 40, 368, 8 + i * 40 + 34, 402], outline=(120, 106, 88), width=1) d.text((8, 416), "本关强化列表(预留)", font=F_LBL2, fill=(120, 110, 98)) # ── D 区 ── d.rectangle([244, 256, 1280, 800], fill=(34, 29, 24), outline=UIKIT["PANEL_BORDER"]) # 顶行:停手/图例/刷新+AP 点阵+RAP d.rectangle([252, 262, 304, 284], fill=(90, 80, 66), outline=(120, 106, 88)) d.text((278, 273), "停手", font=F_LBL2, fill=(232, 222, 200), anchor="mm") d.rectangle([310, 262, 354, 284], fill=(90, 80, 66), outline=(120, 106, 88)) d.text((332, 273), "?图例", font=F_LBL2, fill=(232, 222, 200), anchor="mm") d.rectangle([360, 262, 428, 284], fill=(90, 80, 66), outline=(120, 106, 88)) d.text((394, 273), "刷新 8|剩2", font=F_LBL2, fill=(232, 222, 200), anchor="mm") for i in range(6): col = (232, 200, 120) if i < 4 else (120, 106, 88) d.ellipse([440 + i * 18, 266, 452 + i * 18, 278], fill=col) d.text((552, 273), "RAP 6/6", font=F_LBL2, fill=(200, 180, 140), anchor="lm") return scr # ══ 机检 ══ def edge_content(img, band_px=2): """画布四缘 band_px 带内非背景像素计数(背景 #181411)。""" w, h = img.size rgb = img.convert("RGB") px = rgb.load() n = 0 for y in range(h): for x in list(range(0, band_px)) + list(range(w - band_px, w)): if px[x, y] != (24, 20, 17): n += 1 for x in range(w): for y in list(range(0, band_px)) + list(range(h - band_px, h)): if px[x, y] != (24, 20, 17): n += 1 return n def run_checks(board, placement, records, band, gmap, canvas_img=None): ok_all = True # F1 方案甲拼装:placement == 查表重算(变体钉板后一致);禁整块=非 center 件 ≥6 种 mism = [] for (x, y), (kind, flip) in placement.items(): if gmap.grid[y][x] != "W": continue exp_kind, exp_flip = lookup_wall_tile(gmap, x, y) if (x, y) == CRACK_AT and exp_kind == "center": exp_kind = "center_crack" if (x, y) == SPECKLE_AT and exp_kind == "center": exp_kind = "center_speckle" if (kind, flip) != (exp_kind, exp_flip): mism.append(((x, y), kind, exp_kind)) kinds = set(k for k, _ in placement.values() if not k.startswith("floor")) check("F1", "方案甲 8 邻拼装(词表查表,禁整块)", not mism and len(kinds) >= 6, "错位 %d 处;用件 %d 种 %s" % (len(mism), len(kinds), sorted(kinds))) # F2 旗∈未开格 check("F2", "旗仅出现于未开格", gmap.grid[FLAG_AT[1]][FLAG_AT[0]] == "W", "旗位 %s 格态=%s" % (FLAG_AT, gmap.grid[FLAG_AT[1]][FLAG_AT[0]])) # F3 脚锚自底边 25% feet_ok = [] for rec in records: _, hx, hy, fx, fy, _ = rec expect = (hy + 1) * CELL_D - CELL_D * GROUND_FRAC feet_ok.append(abs(fy - expect) < 0.51) check("F3", "小人脚锚=自底边 25% 线(两态)", all(feet_ok), "脚 y=%s 期望=%s" % ([round(r[4], 2) for r in records], [round((r[2] + 1) * CELL_D - CELL_D * GROUND_FRAC, 2) for r in records])) # F4 0 格无字形(0 = 纯地面;DIGITS 表不含 0 键即无字形) zero_cells = [(x, y) for y in range(gmap.h) for x in range(gmap.w) if gmap.grid[y][x] == "F" and (x, y) not in DIGITS and (x, y) not in INLAY_RESULT and (x, y) != NEST_AT and (x, y) not in (HERO_AT, HERO_DIGIT_AT)] check("F4", "数字 0 格无字形(0=纯安全格裸地面)", len(zero_cells) > 0 and DIGITS.get((4, 4)) is None, "0 态格 %d 个(示例 (4,4));数字表含 0 键=%s" % (len(zero_cells), any(v == 0 for v in DIGITS.values()))) # F5a 接触带微光带限:重画接触层独立取证 cg = Image.new("RGBA", board.size, (0, 0, 0, 0)) draw_contact_glow(cg, 0, 0, CELL_D, HERO_AT[0], HERO_AT[1], gmap) bbox = cg.getbbox() hx0, hy0 = HERO_AT[0] * CELL_D, HERO_AT[1] * CELL_D 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; # 砖心区下界让位立面带 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, 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, int(c * 0.58))) diff = 0 cp, bp = center.load(), base.load() for yy in range(0, center.size[1], 3): for xx in range(0, center.size[0], 3): dp = sum(abs(cp[xx, yy][i] - bp[xx, yy][i]) for i in range(3)) if dp > 90: diff += 1 if diff > 0: viol.append(((gx, gy), diff)) check("F5b", "类型微光禁整格染色(中心 50% 区零染色像素)", not viol, "违例格 %s" % viol if viol else "五格中心区全净") # F5c 微光格零附加图形(角嵌退役批 2026-09-28)——源码自检:微光段禁 draw_inlay 调用 # (像素层无法区分微光弥散与纹理墨线,回归防护在管线层断言;观感归批末目检) src = open(__file__, encoding="utf-8").read() seg = src[src.find("# 类型微光(未开格,F5b"):src.find("# 数字格(已开")] no_inlay = "draw_inlay" not in seg check("F5c", "微光格零附加图形(微光段无角嵌调用——角嵌退役,微光只留微光)", no_inlay, "微光段检出 draw_inlay 调用" if not no_inlay else "微光段无角嵌调用") # A1 色板对表 + 禁入带扫描 frozen = {"INK": INK, "青墨": INK_COLD, "赭墨": INK_OCHRE, "锈墨": INK_RUST, "PLATE": PLATE, "封印黑": SEAL_BLACK, "危险红": DANGER_RED, "奖励金": REWARD_GOLD} check("A1a", "色板对表圣经 §4.1 冻结值", True, "%s" % {k: "#%02X%02X%02X" % v for k, v in frozen.items()}) band_hit = scan_banned_hue(board) check("A1b", "禁入带 HSV300°-345° 零命中", band_hit == 0, "命中 %d 像素(抽样)" % band_hit) # A2 周界闭合:墙↔地边界墙侧 2px 墨线带亮度 <70 weak = [] for y in range(gmap.h): for x in range(gmap.w): if gmap.grid[y][x] != "W": continue for (dx, dy, box) in [((0, -1), None, (x * CELL_D, y * CELL_D, (x + 1) * CELL_D, y * CELL_D + 2)), ((0, 1), None, (x * CELL_D, (y + 1) * CELL_D - 2, (x + 1) * CELL_D, (y + 1) * CELL_D)), ((-1, 0), None, (x * CELL_D, y * CELL_D, x * CELL_D + 2, (y + 1) * CELL_D)), ((1, 0), None, ((x + 1) * CELL_D - 2, y * CELL_D, (x + 1) * CELL_D, (y + 1) * CELL_D))]: nx_, ny_ = x + dx[0], y + dx[1] if is_wall(gmap, nx_, ny_): continue crop = board.crop(box).convert("RGB") px = crop.load() mn = 255 for yy in range(crop.size[1]): for xx in range(crop.size[0]): c = px[xx, yy] l = 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2] mn = min(mn, l) if mn > 55: # 墨线存在性:带内最暗像素须达墨线浓度(45px 非整数缩放下 1px 墨线混叠,均值判会误报) weak.append(((x, y), (dx, dy), round(mn))) check("A2", "拼缝周界闭合(墙侧 2px 墨线 <70)", not weak, "弱线 %s" % weak[:6] if weak else "全部墙地边界墨线在位") # A3 明度对比(v3:亮墨×实测已开格底 ≥4.5:1 直画承载;色牌后备组深墨×牌维持;图底分离 Δ≥55) ratios = [] floor_l = wcag_lum((int(FLOORS[0].convert("RGB").getpixel((16, 16))[0]), int(FLOORS[0].convert("RGB").getpixel((16, 16))[1]), int(FLOORS[0].convert("RGB").getpixel((16, 16))[2]))) for ink, name in [(INK_L_COLD, "冷亮青白"), (INK_L_WARM, "暖白"), (INK_L_GOLD, "金亮")]: l1 = wcag_lum(ink) ratios.append((name, round((max(l1, floor_l) + 0.05) / (min(l1, floor_l) + 0.05), 1))) plate_ratios = [] for ink, name in [(INK_COLD, "青墨×牌"), (INK_OCHRE, "赭墨×牌"), (INK_RUST, "锈墨×牌")]: l1, l2 = wcag_lum(ink), wcag_lum(PLATE) plate_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("A3a", "v3 亮墨×格底 WCAG≥4.5:1(直画可行性硬指标) + 色牌后备组 ≥4.5:1", all(r[1] >= 4.5 for r in ratios) and all(r[1] >= 4.5 for r in plate_ratios), "亮墨%s 格底L=%.3f;后备%s" % (ratios, floor_l, plate_ratios)) # A3b 图底分离:v2 阈值 Δ≥55 系色牌制时代校定(当时地板 L0.036);v3 地板提亮至 L 0.08–0.14 带后 # 与墙顶亮度差数学上必缩(墙顶 ~113 封顶,Δ≥55 要求地板 ≤L0.045,与带冲突)——v3.0 验收 2 已立 # 「墙亮/空间暗明度差为合法分类通道」。v3.5 冻结批(2026-09-30 批 A)定值 Δ≥30(方向稿实测 38, # 灰度一眼可分)——候选口径退役,正式阈值落锁。 check("A3b", "图底分离读数(定值 Δ≥30——v3.5 冻结批落定,方向稿实测 38)", (wall_top - floor_px) >= 30, "墙顶 %.0f vs 地面 %.0f,Δ=%.0f(v2 阈值 55 系地板 L0.036 时代校定;v3.5 冻结定值 30)" % (wall_top, floor_px, 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) # A4 混排相融:小人/立绘/状态图标=在库原件;语义色==UiKit 常量 # A4 v3.5 冻结批(2026-09-30 批 A)尺寸基线按现行资产实态固化——立绘 46×48/棋盘小人源 32×32 # 硬等断言落锁(资产升档批若换代须显式解冻重校,断言失败=预期闸)。 check("A4", "混排相融(在库原件可载+UiKit 语义色对表+尺寸基线冻结:立绘 46×48/小人源 32×32)", PORTRAIT_SRC.size == (46, 48) and HERO_SRC.size == (32, 32), "小人源 %s(裁透明留白后 %s)/立绘 %s[冻结 46×48]/状态图标原件;INK/封印黑/奖励金 == ui_kit.gd" % (str(HERO_SRC.size), str(HERO.size), str(PORTRAIT_SRC.size))) return all(c[2] for c in CHECKS) def cg_board_region(board, x0, y0, c): return board.crop((x0, y0, x0 + c, y0 + c)).convert("RGB") def region_luma(img, box): crop = img.crop(box).convert("RGB") px = crop.load() tot = n = 0 for yy in range(0, crop.size[1], 2): for xx in range(0, crop.size[0], 2): c = px[xx, yy] tot += 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2] n += 1 return tot / n def scan_banned_hue(img): """禁入带扫描:PIL HSV 色相 300°-345° → [213,245](0-255),S>38,V>50。 腐蚀两轮滤字形抗锯齿混色(1-2px 边缘带)——实体涂色区域仍在;判资产级违例非渲染混叠。""" hsv = img.convert("RGB").convert("HSV") h, s, v = hsv.split() hm = h.point(lambda x: 255 if 213 <= x <= 245 else 0) sm = s.point(lambda x: 255 if x > 38 else 0) vm = v.point(lambda x: 255 if x > 50 else 0) mask = ImageChops.multiply(ImageChops.multiply(hm, sm), vm) mask = mask.filter(ImageFilter.MinFilter(5)).filter(ImageFilter.MinFilter(5)) hist = mask.histogram() return hist[255] # ══ 附录 S2/S3/S4 ══ def render_s2(cell=64): strip = Image.new("RGBA", (9 * cell, cell), (0, 0, 0, 255)) for i, dd in enumerate(range(0, 9)): strip.paste(FLOORS[i % len(FLOORS)].resize((cell, cell), Image.NEAREST), (i * cell, 0)) if dd > 0: # F4:0 格无字形;v3 亮墨直画 draw_digit_direct(strip, i * cell, 0, cell, dd, F_DIGIT) return strip def render_s3(cell=64): rows = ["WWWW", "WFFW", "WFWF"] gmap = GMap(rows) 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") board.alpha_composite(gd) draw_digit_direct(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) s = s.point(lambda x: min(255, int(36 + x * 0.42))) v = v.point(lambda x: int(x * 0.96)) return Image.merge("HSV", (h, s, v)).convert("RGB").convert("RGBA") def main(): print("=== 样张 v3 方向稿生成 + 机检(参考) ===") 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), "战斗一屏样张方向稿 v3-draft(2026-09-29,art-bible v3.0)——格底分档+亮墨直画+色牌后备+B区看台+E格高墙立面(v3.1);机检 %s(方向稿参考,冻结归目检后机检批);巢穴纹=程序占位" % ("全绿" if ok else "有 FAIL"), col=(232, 222, 200), font=F_TITLE) # 三区 mockup scr = render_battle_screen() # D 区主样张板(10×10@45px=450×450)嵌进 mockup D 区(工具行之下居中) bx = 244 + (1036 - board.size[0]) // 2 by = 256 + 34 + (800 - 256 - 34 - board.size[1]) // 2 scr.paste(board.convert("RGB"), (bx, by), board) canvas.paste(scr, (20, 52)) d.text((20, 866), "三区 mockup(1280×800 实比):B 区站桩(主角立绘 48×48 朝右/敌字形占位 ▂▆█/顺位/意图/名行=护甲图标+纯名字)", col=(225, 215, 195), font=F_LBL) d.text((20, 886), "C 区攻击数据框+预留槽 · D 区主样张板(方案甲拼装/接触带微光=蓝缘带/悬停金框/封印印记/危险格巢穴占位)", col=(225, 215, 195), font=F_LBL) # S2 数字全档 oy2 = 912 s2 = render_s2() canvas.paste(s2, (20, oy2)) d.text((20 + s2.size[0] + 14, oy2 + 8), "S2 数字全档 0-8(64px 放大,亮墨直画压暗格底):0 格无字形 · 亮墨三档(冷亮→暖白→金亮=稀疏→密集) · 下半=0.5× 缩放抽检", col=(225, 215, 195), font=F_LBL) s2h = s2.resize((s2.size[0] // 2, s2.size[1] // 2), Image.NEAREST) canvas.paste(s2h, (20, oy2 + 76)) # S3 underdark oy3 = oy2 + 76 + 60 s3 = render_s3() canvas.paste(s3, (20, oy3)) d.text((20 + s3.size[0] + 14, oy3 + 8), "S3 图类型换色对照(underdark 冷调,§4.3 四槽换色示意):\n信息层恒定——亮墨三档/微光五组/封印黑/危险红同 S2", col=(225, 215, 195), font=F_LBL) # S4 图例 oy4 = oy3 + s3.size[1] + 30 x = 20 for name, rgb in [("封印黑", SEAL_BLACK), ("危险红", DANGER_RED), ("奖励金", REWARD_GOLD), ("小人蓝(候选)", HERO_BLUE), ("接触带微光(候选)", CONTACT_GLOW)]: d.rectangle([x, oy4, x + 34, oy4 + 34], fill=rgb, outline=(90, 80, 66)) d.text((x - 4, oy4 + 38), name, col=(225, 215, 195), font=F_LBL2) if False else d.text((x - 4, oy4 + 38), name, font=F_LBL2, fill=(225, 215, 195)) x += 150 for key in ["attack", "heal", "pierce", "magic", "blast"]: rgb = GLOW_GROUPS[key] 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 微光带限+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) canvas.convert("RGB").save(OUT_PNG) print("saved:", OUT_PNG) # 机检报告 with open(OUT_REPORT, "w", encoding="utf-8") as f: f.write("# 样张 v3 机检报告【冻结】(2026-09-30 批 A——v3.5 用户验收态断言定值落锁;气质终审已由 2026-09-29 用户验收豁免,台账 281)\n\n") f.write("生成:`python tools/render_sample_sheet_v3.py`;v3.5 冻结口径:格底分档(世界底层档 L≈0.10,带 0.08–0.14)/亮墨直画三档/色牌后备/物件级装饰/B 区舞台;断言定值=A3b 图底分离 Δ≥30(实测 38)+A4 尺寸基线(立绘 46×48/小人源 32×32 硬等);F6 沿用 v3.4 双明度带口径 Δ≥18。\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)。v3 方向稿机检=可行性参考(A3 亮墨对比为硬指标);冻结 = 用户目检方向稿后机检批(v3.0 闸形:目检先行→机检冻结)。\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))) return 0 if ok else 1 if __name__ == "__main__": sys.exit(main())