diff --git a/production/mockups/sample-batch-v1-2026-09-26.png b/production/mockups/sample-batch-v1-2026-09-26.png new file mode 100644 index 00000000..3aaead5e Binary files /dev/null and b/production/mockups/sample-batch-v1-2026-09-26.png differ diff --git a/production/mockups/sample-v1-机检报告-2026-09-26.md b/production/mockups/sample-v1-机检报告-2026-09-26.md new file mode 100644 index 00000000..d805c174 --- /dev/null +++ b/production/mockups/sample-v1-机检报告-2026-09-26.md @@ -0,0 +1,20 @@ +# 样张 v1 机检报告(2026-09-26) + +生成:`python tools/render_sample_sheet.py`;断言事实源 = 画法参数四源对照表-2026-09-26.md §6。 + +| 断言 | 判据 | 结果 | 明细 | +|---|---|---|---| +| F1 | 方案甲 8 邻拼装(词表查表,禁整块) | PASS | 错位 0 处;用件 15 种 ['center', 'center_crack', 'center_speckle', 'end_n', 'end_s', 'end_w', 'hrun', 'in_nw', 'in_sw', 'n', 'nw', 's', 'sw', 'vrun', 'w'] | +| F2 | 旗仅出现于未开格 | PASS | 旗位 (1, 2) 格态=W | +| F3 | 小人脚锚=自底边 25% 线(两态) | PASS | 脚 y=[213.75, 258.75] 期望=[213.75, 258.75] | +| F4 | 数字 0 格无字形(0=纯安全格裸地面) | PASS | 0 态格 40 个(示例 (4,4));数字表含 0 键=False | +| F5a | 接触带微光⊆英雄格周界(空间侧,不进墙格) | PASS | 微光 bbox=(86, 176, 119, 230) 英雄格=(90, 180, 135, 225) | +| F5b | 类型微光禁整格染色(中心 50% 区零染色像素) | PASS | 五格中心区全净 | +| A1a | 色板对表圣经 §4.1 冻结值 | PASS | {'INK': '#33261B', '青墨': '#132E36', '赭墨': '#3C250C', '锈墨': '#4A1E14', 'PLATE': '#F5EAD3', '旗朱': '#C63D22', '危险红': '#A8321F', '奖励金': '#F0A828'} | +| A1b | 禁入带 HSV300°-345° 零命中 | PASS | 命中 0 像素(抽样) | +| A2 | 拼缝周界闭合(墙侧 2px 墨线 <70) | PASS | 全部墙地边界墨线在位 | +| A3 | 墨×牌 WCAG≥4.5:1 + 图底分离 | PASS | [('青墨', 12.0), ('赭墨', 12.0), ('锈墨', 11.8)];墙顶 113 vs 地面 51 | +| A5 | 画布四缘 2px 净空(零溢出) | PASS | 缘带内容像素 0 | +| A4 | 混排相融(游戏在库资产+UiKit 语义色) | PASS | 小人源 32×32(裁透明留白后 (19, 20))/立绘 48×48/状态图标 32×32 原件;INK/旗朱/奖励金 == ui_kit.gd | + +结论:**全绿**(12/12 PASS)。气质终审 = 用户(异步,不阻塞 T2-T7)。 diff --git a/production/mockups/画法参数四源对照表-2026-09-26.md b/production/mockups/画法参数四源对照表-2026-09-26.md new file mode 100644 index 00000000..d6aa2340 --- /dev/null +++ b/production/mockups/画法参数四源对照表-2026-09-26.md @@ -0,0 +1,74 @@ +# 画法参数四源对照表(样张 v1 · T1 对表先行,2026-09-26) + +> 任务书:`.zcode/plans/battle-stage-node2d-loop-plan-2026-09-26.md` T1①。四源 = A battle.md 布局线框 / B 机制代码(格态·旗语义·脚锚)/ C 引擎常量 / D art-bible 画法章节。**一处对不上 = L3**。本表逐参数登记取值与出处;样张 v1 脚本(tools/render_sample_sheet.py)只准消费本表参数。 + +## 1. 屏面布局(源 A:battle.md §Layout + §2 现状) + +| 参数 | 取值 | 出处 | +|---|---|---| +| 基座分辨率 | 1280×800 | art-bible §8.1 两档定案 | +| B 区 | 全宽 × 252px:主角站桩(左)+ 兑现演出区(中)+ 敌方站桩 ×1–3(右);顶部中央轮次·阶段指示;底部结算明细带(挖掘期收起 24px) | battle.md §Layout / battle_stage_view.gd `_init`(custom_minimum_size 0×252) | +| C 区 | 左下 240px × 548px:攻击数据框(攻击值+修正来源角标+护甲 AC)+ 魔法道具槽(预留 4)+ 本关强化列表(预留) | battle.md §Layout(2026-09-26 信息归属批后口径)/ hero_info_panel_view | +| D 区 | 右侧余幅(~1036px):顶行 AP 点阵+RAP+弃图刷新+图例+停手;下为棋盘 | battle.md §Layout / ap_bar_view | +| 站桩盒 | 150×176:血条(10px)+状态行(16px)头顶悬浮;立绘盒 96px;底座底条(10px);敌名行(护甲图标+纯名字,12px);顺位徽标①②③ | standee_view.gd `_init` 几何 | +| B 区立绘 | 主角 = 48×48 朝右贴图(96px 盒 = 48×2 整数档);敌人 = 字形占位(▂地精/▆熊地精/█食人魔酋长——素材零张在库) | battle_stage_view.gd:38 / standee_view.gd `_refresh_portrait_glyph` / §2 现状「敌人立绘素材一张都不存在」 | + +## 2. 格态与语义(源 B:机制代码) + +| 参数 | 取值 | 出处 | +|---|---|---| +| 格态 | 未开(墙)/ 已开(地面)两态;雷 = 已开态巢穴纹更凶一档 | board_view.gd paint 分支 / art-bible §3.2 地砖条 | +| 数字 0 格 | **无字形**(`if d > 0: glyph = str(d)`——0 = 纯安全格裸地面) | board_view.gd:583-586(FATAL④ 事实源) | +| 旗语义 | 旗只插**未开格**(旗分支在 `else` 未开侧);旗 = 封印印记呈现名;旗面恒画右 | board_view.gd:589-590 / art-bible §3.2 旗条(FATAL② 事实源) | +| 悬停金框 | 预承诺光标态画在目标格(逐格指认=悬停金框) | art-bible §3.2 接触带条尾句 / board_view `_apply_hover_visuals` | +| 小人同格 | 小人与数字同格:数字色牌不废、蓝框不覆盖文本 | art-bible §1.4 小人两态条 | + +## 3. 引擎常量(源 C:board_view.gd / standee_view.gd / ui_kit.gd) + +| 参数 | 取值 | 出处 | +|---|---|---| +| 格边长(1280 基座) | 45px(10×10 板)/ 36px(12×12 板);样张 D 区按 45px 实比 | art-bible §8.1 / board_view clamp(26,46) | +| 小人脚锚 | **自底边往上 25% 线**(HERO_GROUND_FRAC=0.25;⚠ 纵向比例一律自底边口径) | board_view.gd HERO_GROUND_FRAC(FATAL③ 事实源) | +| 小人档 | 32×32 源 ×1.5 = 48px(台账 161 用户实机拍板档;45px×32 源非整数 NEAREST 已接受——台账 166 同级) | board_view.gd HERO_SPRITE_SCALE | +| 椭圆影 | 贴脚(中心=脚锚),32×12px @1×,黑 35% | board_view.gd HERO_SHADOW_PX / 台账 162 | +| 立绘缩放 | B 区 96px 盒 = 48 源 ×2 整数(C5) | standee_view.gd set_portrait_texture | +| 变异遮蔽 | hide_number 数字格显示 `?`(样张不含变异格——六类型纹带另批) | board_view.gd:584 | +| 游戏 UiKit 语义色 | INK #33261B / FLAG_RED #C63D22 / GOLD #F0A828 / RED_DEEP #963C30 / 数字=INK | ui_kit.gd 常量(A4 混排对表基准) | + +## 4. 画法章节(源 D:art-bible) + +| 参数 | 取值 | 出处 | +|---|---|---| +| 未开格 | **方案甲高墙瓦片 8 邻拼装**:16 唯一件+运行时 flip_h 镜像(禁旋转);词表=board-tile-vocab-plan-a;板外视为墙;center 变体 crack/speckle 60/20/20 哈希分配 | §3.2 高墙瓦片体系条 / design/research/board-tile-vocab-plan-a-2026-09-26.md(FATAL① 事实源) | +| 墨线纪律 | 墙/地分界 1px 深墨线贴格边(缘件自带,实测朝地边亮度 ~22-36 vs 本体 ~122);南缘立面窄暗条;北缘内侧 1px 提亮 | §3.2 墨线纪律 / v9.3 实测 | +| 图底分离 | 已开地面明度 < 未开墙顶(实测 Δ≈75,0-255 简易亮度) | §8.5 图底分离条 | +| 已开格 | 地面瓦片 5 变体哈希;产资源格 = 家族物件纹样(道具点剪影)角嵌砖缘+**砖心留白予数字色牌**;纯安全格 = 干净裸地面 | §3.2 结果整格表达 / §1.2 原则三 | +| 数字色牌 | #F5EAD3 素色直角小牌,恒定不随格底;数字墨三档:1-3 青墨 #132E36 / 4-5 赭墨 #3C250C / 6-8 锈墨 #4A1E14;对牌对比 ≥4.5:1(实测 11.8-12.3:1);字号下限 ≥18px | §4.1 / HUD框架 基线表 | +| 语义色 | 掩尘 #C9AF83 / 裸土 #B39A78 / 旗朱 #C63D22 / 危险红 #A8321F / 奖励金 #F0A828 | §4.1 冻结 | +| 类型角嵌 | 未开格四角/边框墨族单色纹样(九家族键 §4.6.3),砖心留白;形状主通道 | §3.2 类型角嵌条 | +| 类型微光 | 五组候选:通用增益 #D6C13A / 治疗 #5CB83A / 穿刺 #2E9E9E / 魔法 #7B5BD6 / 群伤 #FF2D55;底效泛光层不入遮挡栈;**禁整格染色**(v1 画法=内缘带状泛光,机检断言中心 50% 区零微光像素);不以明度增量承载信息 | §4.2 类型微光色带 / §1.4 验收 2(FATAL⑤ 事实源之一) | +| 接触带微光 | 英雄格周界**空间侧**:正交邻墙=整条边、斜向=共享角点+两侧各半条边;蓝族浅一档;光不进墙格 | §3.2 可挖域接触带微光条 / §2.1(FATAL⑤ 事实源之二) | +| 禁入带 | HSV 色相 300°–345° 全场禁入(微光群伤组 349° 刻意避开) | §4.2 紫赤条 | +| 换皮 | §4.3 四槽换色(tile_closed/tile_open/wall_ambient/variant_tint),信息层恒定;样张 ≥两套(dungeon+underdark) | §4.3 / §1.4 内容清单 | +| 小人占位 | 独立资产 32×32;样张 48px ≈ 0.75× 格边符合 §8.2 占位上限 | §8.2 画布基准 | + +## 5. 候选值与偏差登记(非矛盾,样张批冻结项) + +1. **小人蓝**:art-bible §4.1 = #2E86DE(候选,样张批冻结终稿);游戏 UiKit = #2E6E8E。两值并存系圣经明标「候选值」——样张以 #2E86DE 出图;冻结后游戏侧换色归循环 2,本批不改代码。 +2. **接触带微光色值**:空间侧+蓝族已定稿(v2.8),具体色值样张批冻结——v1 取小人蓝族浅两档 #D6ECFF(近白蓝)出样。 +3. **格底 L≥0.30 的适用面**:§4.1/§4.3 系换皮四槽**色槽参数域**(新图类型色板须守);dungeon 定稿瓦片(v9.3 用户验收入本体)为素材实态,本样张不再以 L≥0.30 复裁(防对已验收资产误报 FATAL),改以图底分离+可读性硬指标承载。 +4. **巢穴纹/封印印记**:素材缺口,程序化占位画面内标注(§1.4 允许——「素材缺口见清单」v0 先例);正式纹样归绘画批。 + +## 6. 机检断言映射(v0 五画错 + 程序化四件 → 生成脚本自检) + +| 断言 | 判据 | +|---|---| +| F1 方案甲拼装 | 每墙瓦片位 = 词表查表(四邻+对角)结果;禁整块(center 之外的件 ≥6 种实出);变体 60/20/20 | +| F2 旗∈未开格 | 旗位集 ⊆ 墙位集 | +| F3 脚锚自底 25% | 两小人态脚 y = 格底 − 0.25×45;影心=脚点 | +| F4 0 格无字形 | 数字 0 格 glyph 记录 = 空 | +| F5 微光带限 | 接触带微光像素 ⊆ 英雄格∩带几何(≤8px 距离);类型微光中心 50% 区零像素 | +| A1 色板+禁入带 | 冻结色逐字节对表 §4.1;微光五组对表 §4.2;全图像素扫 HSV 300°-345°(S>0.15,V>0.20)零命中 | +| A2 周界闭合 | 每段墙↔地边界墙侧 2px 带均亮度 <70(墨线在位);墙群外轮廓无缺口 | +| A3 明度对比 | 三档墨×色牌 WCAG ≥4.5:1;地面均亮 < 墙顶均亮 −40 | +| A4 混排相融 | 语义色 == 游戏 UiKit 常量(小人蓝候选偏差见 §5.1);小人/立绘/状态图标 = 游戏在库资产原件 | diff --git a/production/session-state/active.md b/production/session-state/active.md index 69ab75b9..f565b2ba 100644 --- a/production/session-state/active.md +++ b/production/session-state/active.md @@ -455,4 +455,5 @@ 164. **高墙瓦片 v9.3 定稿交付+备选案清场批(2026-09-26,本会话——用户验收 v9.3 后指令「直接移入游戏本体+备选案打包挪走」;零代码、素材不入 git,worktree 不适用)**:**定稿交付**=21 枚入 `game/assets/art/board/`(16 唯一墙瓦片〔center/center_crack/center_speckle/n/s/w/nw/sw/hrun/vrun/end_n/end_s/end_w/lone/in_nw/in_sw〕+5 地面瓦片变体,`terrain_*_dungeon.png`;东/NE/SE/端头E/凹角NE/SE=运行时 flip_h 镜像不出文件,旧 beast-cave 砖零触碰)。**终版路线**=材质归模型(用户 Gemini 生图①岩石/④泥土→周期对齐无缝裁剪+BOX 降采样 32×32+环绕混合+量化 12 色)+结构归程序(v9 墨线纪律:全开缘 1px 深墨线直线零抖动/北缘内侧 1px 就地提亮/南缘立面窄暗条 11px/凹角角点+立面侧 1px 齐平收头——v9.1 悬空/v9.2 突出/v9.3 齐平三轮定位,前后对比+逐行像素审计存 `_work/corner_cmp_v91_v93_5x.png`)。**术语共识(用户立)**:未开格=墙瓦片,已开格=地面瓦片。**备选案清场**=v1–v9.2 全迭代+被否素材(含 v7 程序砌石/v3 生图 5×5 表)打包 `备选案-terrain-walls-v1-v9_2-2026-09-26.zip`(383 项 7.5MB,用户挪出库),`_work/` 仅留生图①④源图+风格参考+终版证据+定稿件。**复盘机制落记忆**(pixel-structure-verify-discipline):先抄后造/验收口径写像素级视觉判据/±1px 一律坐标推导/修复默认附前后对比。**遗留**:underdark/deep_underdark 换色(AI 纹理色调映射)+实机查表合成器(BoardWorld 接线,含镜像翻转)归后续批;任务书验收单逐条结果以本批 v9.3 终测为准(图底分离 67.6/无死黑/墨线周界闭合/覆盖层可读)。 -168. **战斗舞台 Node2D 循环 1 · T0 开工批(2026-09-26,本会话——计划书 .zcode/plans/battle-stage-node2d-loop-plan-2026-09-26.md 已批准从 T0 自治开跑;worktree battle-stage-node2d-20260926)**:worktree 自 master 70d2320 建+editor --quit 刷缓存 0 error;git status 归属核对=主区脏文件(.gitignore/.zcode config/ui-reuse 指引/playnote/project.godot/loop-dashboard/active.md 165-167 未提交并行登记+untracked 美术文档组)均非本批只标旗不碰。**基线复读全绿记档**——冒烟 557/0+五探针 PASS(ui_probe cells=144 board=12x12/ui_click 问题=0/ui_tune 全链/ui_layout dump:stage 1280×252·board_view 1036×965·info_panel 240×1028/ui_input 十一断言)+static_scan exit0+文档三闸基线存档(死链 7/退役词命中/gdd_lint 8 文件 8 不通过 69 项级)+MC 12 局 both(max 路由) 24 行 RUNSUM sha256=de8225b3 存 /tmp 供 T7 逐位对拍。**§2 现状事实逐条 grep 复核零漂移**——battle_stage_view(PanelContainer/252px/五成员/setter 面)+standee_view(Control/行为面 17 方法/slot_row compact/字形占位)+hud_presenter 53-54/79-80 单投喂+main 309/441/503+ui_assembler 23+ui_probe 95-129/ui_click 223 穿透面+ui_input_probe 零 B 区耦合(grep stage/standee/enemy=0 命中)+TEX_HERO_PORTRAIT 素材 worktree 缺位=C7 回退字形路径预期。**Alpha 清单初稿落盘** production/alpha-acceptance-battle-screen.md——六源拼装(battle.md AC+§13 义务表/HUD框架 验收标准+基线表/art-bible §8 判据/07§⑨ 试玩指引/06 挂 Alpha 条目备查/工程回归面),七节 30 条=判据+验法三档(headless/真机渲染/人)+状态列,销项归 Alpha 验收批。轻闸全绿,进入 T1。 \ No newline at end of file +168. **战斗舞台 Node2D 循环 1 · T0 开工批(2026-09-26,本会话——计划书 .zcode/plans/battle-stage-node2d-loop-plan-2026-09-26.md 已批准从 T0 自治开跑;worktree battle-stage-node2d-20260926)**:worktree 自 master 70d2320 建+editor --quit 刷缓存 0 error;git status 归属核对=主区脏文件(.gitignore/.zcode config/ui-reuse 指引/playnote/project.godot/loop-dashboard/active.md 165-167 未提交并行登记+untracked 美术文档组)均非本批只标旗不碰。**基线复读全绿记档**——冒烟 557/0+五探针 PASS(ui_probe cells=144 board=12x12/ui_click 问题=0/ui_tune 全链/ui_layout dump:stage 1280×252·board_view 1036×965·info_panel 240×1028/ui_input 十一断言)+static_scan exit0+文档三闸基线存档(死链 7/退役词命中/gdd_lint 8 文件 8 不通过 69 项级)+MC 12 局 both(max 路由) 24 行 RUNSUM sha256=de8225b3 存 /tmp 供 T7 逐位对拍。**§2 现状事实逐条 grep 复核零漂移**——battle_stage_view(PanelContainer/252px/五成员/setter 面)+standee_view(Control/行为面 17 方法/slot_row compact/字形占位)+hud_presenter 53-54/79-80 单投喂+main 309/441/503+ui_assembler 23+ui_probe 95-129/ui_click 223 穿透面+ui_input_probe 零 B 区耦合(grep stage/standee/enemy=0 命中)+TEX_HERO_PORTRAIT 素材 worktree 缺位=C7 回退字形路径预期。**Alpha 清单初稿落盘** production/alpha-acceptance-battle-screen.md——六源拼装(battle.md AC+§13 义务表/HUD框架 验收标准+基线表/art-bible §8 判据/07§⑨ 试玩指引/06 挂 Alpha 条目备查/工程回归面),七节 30 条=判据+验法三档(headless/真机渲染/人)+状态列,销项归 Alpha 验收批。轻闸全绿,进入 T1。 +169. **样张 v1 制作与机检批(2026-09-26,本会话——循环 1 T1;worktree battle-stage-node2d-20260926 批次本提交)**:①**四源对照表先行** production/mockups/画法参数四源对照表-2026-09-26.md(battle.md 布局/机制代码〔0 格无字形 board_view:586·旗只插未开 :589·脚锚自底 25%〕/引擎常量〔45px·1.5× 档·96px 盒〕/art-bible §1.2-§4.6 画法)——候选值偏差两笔登记(小人蓝 #2E86DE 候选 vs UiKit #2E6E8E=样张批冻结项非矛盾;格底 L≥0.30 系换皮槽参数域不复裁已验收 dungeon 瓦片)。②**脚本重写** tools/render_sample_sheet.py v1:方案甲词表查表拼装(16 情形+对角回落 nw>ne>sw>se+flip 镜像+变体 60/20/20 哈希)+三区全内容 1280×800 实比 mockup+S2 数字档 0.5× 抽检+S3 underdark 四槽换色+S4 图例;素材两级回退(worktree→主检出,素材不入 git)。③**机检 12 断言生成即验全绿**——F1 词表拼装零错位 15 种用件/F2 旗∈未开/F3 脚锚自底 25%(两态)〔预筛抓出素材底部 7 行全透明→裁除留白后锚定,视觉脚=锚线;**游戏内 BoardWorld.ensure_hero 同病(视觉脚浮锚线 ~10px)=呈现缺陷登记移交循环 2/接线批**〕/F4 0 格无字形/F5a 接触带微光⊆英雄格/F5b 类型微光禁整格染色(中心 50% 区零像素——角嵌臂长两轮收紧 L9→L5 留白砖心)/A1 色板对表+禁入带 300-345° 零命中(腐蚀两轮滤 AA 混色)/A2 周界闭合(墙侧 2px 带墨线存在性 min<55——45px 非整数缩放下均值判误报改最暗值判)/A3 墨×牌 11.8-12.0:1+图底分离 113 vs 51/A4 混排在库原件/A5 画布四缘净空(溢出永久断言)。④**visual-judge 预筛三轮**(≤3 预算内):R1 四 FATAL→triage=站桩横向重叠/小人脚浮(素材留白)/同格态色牌被小人盖(改 §1.2 栈序数字>小人)/缘件裸拼误报(1px 墨线逐像素 zoom 自证 end_n 三向墨线俱在+15 种用件机检反证,判官 3-6px 采样带均值漏 1px 线)+地精无徽标误报(普攻零表示=设计);R2 两 FATAL=画布右缘溢出×2(说明行+S4 末项)→画布 1560+A5 断言化;R3 窄域复核 pass。⑤**交付**=样张 v1+机检报告+对照表三件已拷主区 production/mockups/(评审产物不入 git 同 v0 先例),**用户异步终审**(过=冻结为循环 2 输入;不过=对表改参重出);终审不阻塞 T2-T7。轻闸全绿(当轮基线=T0 记档),进入 T2。 \ No newline at end of file diff --git a/tools/render_sample_sheet.py b/tools/render_sample_sheet.py new file mode 100644 index 00000000..26ba5e44 --- /dev/null +++ b/tools/render_sample_sheet.py @@ -0,0 +1,852 @@ +# -*- coding: utf-8 -*- +"""战斗一屏样张验收图生成脚本 v1(2026-09-26,循环 1 T1)。 + +v0 四 FATAL 案底(2026-09-26 用户否决)全部断言化——生成即验,不过闸不出图: + F1 未开格 = 方案甲缘件 8 邻拼装(词表查表,禁整块 center 铺满) + F2 旗仅出现于未开格 + F3 小人脚锚 = 自底边 25% 线(⚠ 纵向比例一律自底边口径) + F4 数字 0 格无字形(纯安全格 = 干净裸地面) + F5 微光仅带状:接触带微光只画英雄格周界空间侧;类型微光禁整格染色(中心 50% 区零像素) +程序化四件审计(§9 初级验收授权): + A1 色板对表圣经冻结色板 + 禁入带(HSV 300°-345°)全图扫描 + A2 拼缝周界闭合(墙↔地边界墙侧 2px 墨线带亮度 <70) + A3 明度对比(数字墨×色牌 WCAG ≥4.5:1;图底分离) + A4 与现有素材混排相融(语义色 == 游戏 UiKit 常量;小人/立绘/状态图标 = 在库资产原件) + +一屏样张 = 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;画法参数事实源 = +production/mockups/画法参数四源对照表-2026-09-26.md(四源对表先行产物)。 +输出 = production/mockups/sample-batch-v1-2026-09-26.png + sample-v1-机检报告-2026-09-26.md。 +用法:python tools/render_sample_sheet.py +""" +from PIL import Image, ImageDraw, ImageFilter, ImageChops, ImageFont +import os +import sys +import math +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, "production", "mockups", "sample-batch-v1-2026-09-26.png") +OUT_REPORT = os.path.join(ROOT, "production", "mockups", "sample-v1-机检报告-2026-09-26.md") +T_FONT = asset_root("design", "art", "candidates", "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 +FLAG_RED = (198, 61, 34) # 旗朱 #C63D22 +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": FLAG_RED, "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)] +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) + + +# ══ 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"} # 已开结果格物件纹样 +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) + + +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(digit)) + return (px0, py0, pw, ph) + + +def digit_ink(d): + 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 禁整格染色——中心 50% 区零像素)。""" + layer = Image.new("RGBA", glow_layer.size, (0, 0, 0, 0)) + d = ImageDraw.Draw(layer) + rgb = GLOW_GROUPS[kind] + band = max(3, cell // 11) + d.rounded_rectangle([ox + 2, oy + 2, ox + cell - 3, oy + cell - 3], radius=5, + outline=rgb + (120,), 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: + # 数字色牌叠于小人之上(§1.2 优先级栈:数字(含色牌)>小人标记);退避左上角,蓝框不覆盖文本(§1.4) + d2 = ImageDraw.Draw(board) + pw, ph = 22, 24 + px0, py0 = ox + hx * cell + 4, oy + hy * cell + 4 + d2.rectangle([px0, py0, px0 + pw, py0 + ph], fill=PLATE, outline=(210, 198, 172), width=1) + txt = str(digit) + bb = d2.textbbox((0, 0), txt, font=F_DIGIT_D) + d2.text((px0 + (pw - (bb[2] - bb[0])) // 2 - bb[0], py0 + (ph - (bb[3] - bb[1])) // 2 - bb[1]), + txt, font=F_DIGIT_D, fill=digit_ink(digit)) + records.append(("hero", hx, hy, fx, fy, digit)) + + +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) + board.paste(t.resize((cell, cell), Image.NEAREST), (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) + 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) + for (gx, gy, kind) in GLOW_CELLS: + draw_type_glow(glow, gx * cell, gy * cell, cell, kind) + draw_inlay(board, gx * cell, gy * cell, cell, kind, corner=4, m=5, L=5) # 臂长留白砖心(§3.2——纹样不侵中心 50% 数字占位区) + board.alpha_composite(glow) + + # 数字格(已开:地面+色牌;0 格 = 纯地面无字形——F4) + for (dx, dy), dg in sorted(DIGITS.items()): + if (dx, dy) in INLAY_RESULT: + draw_inlay(board, dx * cell, dy * cell, cell, INLAY_RESULT[(dx, dy)], + corner=1, m=6, L=8) # 物件纹样右上角,砖心留白 + if dg > 0: + draw_digit_plate(board, dx * cell, dy * cell, cell, dg, F_DIGIT_D) + + # 封印旗(未开格,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=FLAG_RED) # 旗面恒画右 + 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) + + # 网格线(屏轴对齐,低对比) + 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(gmap.h + 1): + d.line([(0, i * cell), (gmap.w * cell, i * cell)], fill=(0, 0, 0, 45), width=1) + 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") + # 底座 + d.rectangle([ox + 26, oy + 158, ox + W - 26, oy + 168], fill=(90, 82, 70), outline=(110, 100, 84)) + # 敌名行:护甲类型图标+纯名字(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=(30, 26, 22), 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 全宽 ── + d.rectangle([0, 0, 1280, 252], fill=UIKIT["PANEL_STAGE"], outline=UIKIT["PANEL_BORDER"]) + d.text((640, 14), "第 3 轮 · 挖掘", font=ImageFont.truetype(ZH_FONT, 15), fill=(30, 26, 22), 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=(90, 80, 66)) + d.text((10, 240), "── 第 3 轮 ── 地精 受到 4 点伤害(增伤 ×1)· 熊地精 蓄力(重击预告)…", font=F_LBL2, + fill=(232, 222, 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) + 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)) + 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)) + 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 "五格中心区全净") + # A1 色板对表 + 禁入带扫描 + frozen = {"INK": INK, "青墨": INK_COLD, "赭墨": INK_OCHRE, "锈墨": INK_RUST, "PLATE": PLATE, + "旗朱": FLAG_RED, "危险红": 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 明度对比 + ratios = [] + for ink, name in [(INK_COLD, "青墨"), (INK_OCHRE, "赭墨"), (INK_RUST, "锈墨")]: + l1, l2 = wcag_lum(ink), wcag_lum(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("A3", "墨×牌 WCAG≥4.5:1 + 图底分离", all(r[1] >= 4.5 for r in ratios) and (wall_top - floor_px) >= 40, + "%s;墙顶 %.0f vs 地面 %.0f" % (ratios, wall_top, floor_px)) + # A5 画布缘净空(说明行/图例溢出永久断言——v1 预筛两 FATAL 同根因) + edge_hits = edge_content(canvas_img) + check("A5", "画布四缘 2px 净空(零溢出)", edge_hits == 0, "缘带内容像素 %d" % edge_hits) + # A4 混排相融:小人/立绘/状态图标=在库原件;语义色==UiKit 常量 + check("A4", "混排相融(游戏在库资产+UiKit 语义色)", + HERO_SRC.size == (32, 32) and PORTRAIT_SRC.size == (48, 48), + "小人源 32×32(裁透明留白后 %s)/立绘 48×48/状态图标 32×32 原件;INK/旗朱/奖励金 == ui_kit.gd" + % (str(HERO.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 格无字形 + draw_digit_plate(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") + 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) + # 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("=== 样张 v1 生成 + 机检 ===") + 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;巢穴纹/封印印记/角嵌纹样=程序占位" % + ("全绿" 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 [("旗朱", FLAG_RED), ("危险红", 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 微光带限 + A1 色板禁入带/A2 周界闭合/A3 对比/A4 混排——详见机检报告;气质终审=用户", + 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("# 样张 v1 机检报告(2026-09-26)\n\n生成:`python tools/render_sample_sheet.py`;断言事实源 = 画法参数四源对照表-2026-09-26.md §6。\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" % + ("全绿" 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())