test(render): 双帧像素取证+判据源探针(迭代 30 卡① T2-④)——board_shot_probe 每关抓微光开(生产默认态)/关(golden 基线保护面)双帧:前置 UiKit.motion_scale=0.5 冻结接触带呼吸相位(静态带色,双帧差异唯一定位在类型微光开关),_check_dual_frames 逐格窗口矩形+到格缘距离判据(de < cell÷8——带外含砖心零侵入=禁整格染色/禁越圈)+每生效格须有实绘像素(微光不空转)+开关快照须在关开关前取;读数落 board_dual_frame_readings.json(报告读数表源)。身份解析接生产同源面(main.gd:251 同构:板在守卫+Active 相位守卫+query_reward_identity 只读代理——裸视图不接则作用集恒空、PASS 假绿,本批实跑两次 glow=0 的根因即此;附带排 Callable.bind 动态派发 Object 硬校验坑,改成员现值+直接方法引用)。新建判据源探针 type_glow_shot_probe(夹具面五组齐现:glow_cells=5、groups=[1-5]、changed_outside=0、相邻组实测 CR 最弱对 1.232、组4/组5=1.423)——真实面随板摇号(双帧 glow=2/4/4),五组齐现互辨取证在夹具面承载,两面分工不混。off 帧对 2026-09-30 代 golden 三基线漂移 0.13/0.11/0.10%(≤2%,接触带静态化预期面,契约 6 未触发、基线未重制);render_gate.sh 整闸全绿(含菜单 golden 0.14%)。
This commit is contained in:
1 parent
80ac80ef1a
commit
e5c03eb3ab
4 files changed
+405
-6
No files matched your search
@@ -7,11 +7,22 @@
|
|||||||
# 护栏:headless fail-fast(shot_probe 同款);空纹理取证 fail——取证段失败=闸红,
|
# 护栏:headless fail-fast(shot_probe 同款);空纹理取证 fail——取证段失败=闸红,
|
||||||
# 零字节/空纹理不允许静默落盘;board_meta.json 取样坐标落盘(棋盘视图全局矩形——
|
# 零字节/空纹理不允许静默落盘;board_meta.json 取样坐标落盘(棋盘视图全局矩形——
|
||||||
# golden 前置裁剪坐标源,禁硬编码像素)。
|
# golden 前置裁剪坐标源,禁硬编码像素)。
|
||||||
|
# 迭代 30 卡① T2-④ 双帧取证:每关抓 微光开(生产默认态·Q4 丙案)/关(golden 基线保护面)
|
||||||
|
# 两帧——① 开帧逐像素判定「非生效格零变化」(禁整格染色/禁越圈;生效格内只准落在内缘
|
||||||
|
# 带 = cell÷8)② 每个生效格须有实绘像素(微光不空转)③ 关帧即 golden 对比帧(原名落盘,
|
||||||
|
# 基线 2026-09-30 代不重制——契约 1 off 态基线)。隔离纪律 = 与 type_glow_shot_probe 同款:
|
||||||
|
# 前置 UiKit.motion_scale=0.5 冻结接触带呼吸相位(静态带色),两帧差异唯一定位在类型微光。
|
||||||
extends Node
|
extends Node
|
||||||
|
|
||||||
const AutoPlayer := preload("res://smoke/auto_player.gd")
|
const AutoPlayer := preload("res://smoke/auto_player.gd")
|
||||||
|
const ActionEconomy := preload("res://core/action_economy.gd")
|
||||||
|
|
||||||
var _meta := {} # 棋盘视图实测几何(全局 rect——tools/golden_board_crop.py 取样坐标源)
|
var _meta := {} # 棋盘视图实测几何(全局 rect——tools/golden_board_crop.py 取样坐标源)
|
||||||
|
var _problems: Array = []
|
||||||
|
var _level_readings := {} # 关 → 双帧比对读数(实现报告读数表源)
|
||||||
|
var _cur_settlement = null # 当关 settlement(身份回调读取面——仿生产 run_scope.settlement 现值;
|
||||||
|
# 不用 Callable.bind 传参:bind 的 Callable 动态派发时对类型化参数做 Object 硬校验,
|
||||||
|
# 报「Cannot convert argument 2 from Object to Vector2i」——直接方法引用+成员现值无此坑)
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
@@ -30,6 +41,11 @@ func _ready() -> void:
|
|||||||
push_error("[board_shot] 表加载失败")
|
push_error("[board_shot] 表加载失败")
|
||||||
get_tree().quit(1)
|
get_tree().quit(1)
|
||||||
return
|
return
|
||||||
|
# 双帧单一变量隔离(迭代 30 卡①):冻结接触带呼吸相位——motion_scale<1.0 ⇒ _apply_contact_bands
|
||||||
|
# 施静态色(减动效停格路径)。必须在 rebuild 前置位(rebuild/set_dig_glow_enabled 按当时相位
|
||||||
|
# 重施带色)。golden 侧影响:静态带色=呼吸周期中值,与基线捕获相的像素差最坏 ≈灰度24 于
|
||||||
|
# 接触带像素(占比 <0.4%),≤2% 判据不受影响;停格同时令「非生效格零变化」可精确判定。
|
||||||
|
UiKit.motion_scale = 0.5
|
||||||
var ui_font: Font = load("res://presentation/views/ui_kit.gd").load_font(load("res://presentation/views/ui_kit.gd").FONT_UI)
|
var ui_font: Font = load("res://presentation/views/ui_kit.gd").load_font(load("res://presentation/views/ui_kit.gd").FONT_UI)
|
||||||
var targets := [[1, "L1_dungeon"], [2, "L2_underdark"], [3, "L3_deep_underdark"]]
|
var targets := [[1, "L1_dungeon"], [2, "L2_underdark"], [3, "L3_deep_underdark"]]
|
||||||
for t in targets:
|
for t in targets:
|
||||||
@@ -43,6 +59,7 @@ func _ready() -> void:
|
|||||||
get_tree().quit(1)
|
get_tree().quit(1)
|
||||||
return
|
return
|
||||||
var settlement = runner.current_battle()
|
var settlement = runner.current_battle()
|
||||||
|
_cur_settlement = settlement
|
||||||
for i in range(18):
|
for i in range(18):
|
||||||
var act: Dictionary = AutoPlayer.novice_action(settlement.minesweeper)
|
var act: Dictionary = AutoPlayer.novice_action(settlement.minesweeper)
|
||||||
var kind := String(act.get("kind", "stop"))
|
var kind := String(act.get("kind", "stop"))
|
||||||
@@ -58,7 +75,18 @@ func _ready() -> void:
|
|||||||
if ui_font != null:
|
if ui_font != null:
|
||||||
view.set_world_font(ui_font)
|
view.set_world_font(ui_font)
|
||||||
add_child(view)
|
add_child(view)
|
||||||
|
# 身份解析接线(生产同源——main.gd:251 set_identity_resolver(_identity_lookup)):必须在
|
||||||
|
# rebuild / set_dig_glow_enabled 前接好,二者尾链 _recompute_glow 同步读身份建作用集。
|
||||||
|
# 裸视图不接 = _identity_cb 无效 ⇒ info 恒 null ⇒ 作用集恒空(2026-10-17 首两次实跑
|
||||||
|
# 三关 glow=0 根因——双帧比对空转、PASS 假绿)。现值经 _cur_settlement 成员读入
|
||||||
|
# (仿生产 run_scope.settlement;bind 传参有 Callable 类型校验坑,见成员注)。
|
||||||
|
view.set_identity_resolver(_probe_identity_lookup)
|
||||||
view.rebuild(settlement.minesweeper.current_board(), settlement.minesweeper)
|
view.rebuild(settlement.minesweeper.current_board(), settlement.minesweeper)
|
||||||
|
# 进挖掘相位(生产态 = hud_presenter.set_dig_glow_enabled(not settling) 接线——裸视图默认
|
||||||
|
# _dig_glow=false,接触带与类型微光同源门控都不渲染;golden 基线 2026-09-30 代即捕获于
|
||||||
|
# 该裸态。双帧取证要求生产默认态,故显式开——两帧均含静态接触带(motion_scale=0.5 停格),
|
||||||
|
# 差异唯一定位在类型微光开关)
|
||||||
|
view.set_dig_glow_enabled(true)
|
||||||
# 抓帧前等入场动画完结(迭代 19 卡① S2 观察项根因修复 2026-10-08):rebuild 有全板淡入
|
# 抓帧前等入场动画完结(迭代 19 卡① S2 观察项根因修复 2026-10-08):rebuild 有全板淡入
|
||||||
# motion(0.2)+R0 涟漪逐格延迟显形(motion(0.04×离板心距离)+motion(0.12),最远格 ≈0.56s),
|
# motion(0.2)+R0 涟漪逐格延迟显形(motion(0.04×离板心距离)+motion(0.12),最远格 ≈0.56s),
|
||||||
# 原制 6 个 process_frame 热机后 ≈0.1s=捕在涟漪中途——逐跑帧时序抖动使揭示 alpha 漂移,
|
# 原制 6 个 process_frame 热机后 ≈0.1s=捕在涟漪中途——逐跑帧时序抖动使揭示 alpha 漂移,
|
||||||
@@ -66,21 +94,124 @@ func _ready() -> void:
|
|||||||
await get_tree().create_timer(1.2).timeout
|
await get_tree().create_timer(1.2).timeout
|
||||||
await get_tree().process_frame
|
await get_tree().process_frame
|
||||||
await RenderingServer.frame_post_draw
|
await RenderingServer.frame_post_draw
|
||||||
var img := get_viewport().get_texture().get_image()
|
var on_img := get_viewport().get_texture().get_image()
|
||||||
if img == null or img.is_empty():
|
if on_img == null or on_img.is_empty():
|
||||||
push_error("[board_shot] FAIL — 抓帧空纹理: %s(Q7 取证语义=闸红)" % tag)
|
push_error("[board_shot] FAIL — 抓帧空纹理: %s(Q7 取证语义=闸红)" % tag)
|
||||||
get_tree().quit(1)
|
get_tree().quit(1)
|
||||||
return
|
return
|
||||||
|
var on_path := "%s/board_%s_glow_on.png" % [out, tag]
|
||||||
|
on_img.save_png(on_path)
|
||||||
|
# 作用集快照必须在关开关之前取(set_type_glow_enabled(false) 会清空 _glow 重算)
|
||||||
|
var glow_cells: Dictionary = (view.type_glow_probe().get("cells", {}) as Dictionary).duplicate()
|
||||||
|
# 帧 B = 微光关(golden 基线保护面——与基线同文件名落盘,golden 对比取本帧)
|
||||||
|
view.set_type_glow_enabled(false)
|
||||||
|
await get_tree().process_frame
|
||||||
|
await RenderingServer.frame_post_draw
|
||||||
|
var off_img := get_viewport().get_texture().get_image()
|
||||||
|
if off_img == null or off_img.is_empty():
|
||||||
|
push_error("[board_shot] FAIL — 抓帧空纹理: %s_off(Q7 取证语义=闸红)" % tag)
|
||||||
|
get_tree().quit(1)
|
||||||
|
return
|
||||||
var path := "%s/board_%s.png" % [out, tag]
|
var path := "%s/board_%s.png" % [out, tag]
|
||||||
img.save_png(path)
|
off_img.save_png(path)
|
||||||
_meta[tag] = {"x": view.global_position.x, "y": view.global_position.y,
|
_meta[tag] = {"x": view.global_position.x, "y": view.global_position.y,
|
||||||
"w": view.size.x, "h": view.size.y}
|
"w": view.size.x, "h": view.size.y}
|
||||||
print("[board_shot] saved %s (%dx%d)" % [path, img.get_width(), img.get_height()])
|
print("[board_shot] saved %s (%dx%d) + %s" % [path, off_img.get_width(), off_img.get_height(), on_path])
|
||||||
|
_check_dual_frames(tag, view, glow_cells, off_img, on_img)
|
||||||
view.queue_free()
|
view.queue_free()
|
||||||
await get_tree().process_frame
|
await get_tree().process_frame
|
||||||
var mf := FileAccess.open(out.path_join("board_meta.json"), FileAccess.WRITE)
|
var mf := FileAccess.open(out.path_join("board_meta.json"), FileAccess.WRITE)
|
||||||
if mf != null:
|
if mf != null:
|
||||||
mf.store_string(JSON.stringify(_meta))
|
mf.store_string(JSON.stringify(_meta))
|
||||||
mf.close()
|
mf.close()
|
||||||
print("[board_shot] done")
|
var rf := FileAccess.open(out.path_join("board_dual_frame_readings.json"), FileAccess.WRITE)
|
||||||
get_tree().quit(0)
|
if rf != null:
|
||||||
|
rf.store_string(JSON.stringify(_level_readings, " "))
|
||||||
|
rf.close()
|
||||||
|
if _problems.is_empty():
|
||||||
|
print("[board_shot] PASS — 三关双帧取证:非生效格零变化/生效格内缘带外零侵入/生效格实绘非空;golden 帧=微光关态(契约 1 基线保护面)")
|
||||||
|
get_tree().quit(0)
|
||||||
|
else:
|
||||||
|
for p in _problems:
|
||||||
|
push_error("[board_shot] FAIL — " + p)
|
||||||
|
print("[board_shot] FAIL — " + "; ".join(_problems))
|
||||||
|
get_tree().quit(1)
|
||||||
|
|
||||||
|
|
||||||
|
# 身份解析回调(生产同源面——main.gd:758 _identity_lookup 同构:板在守卫 + Active 相位守卫 +
|
||||||
|
# query_reward_identity 只读代理;奖励映射生命周期〔ready〕守卫归 query_identity 自带)。
|
||||||
|
# 相位为何是 Active:每轮 settle_batch 末尾 _exit_distribution 已 notify_round_start 兑现下轮
|
||||||
|
# AP ⇒ 18 步 novice 驱动后相位回 Active,即生产「挖掘期看板」态;本回调零玩法数据写入
|
||||||
|
# (🔴 契约 1——只读投影面)。
|
||||||
|
func _probe_identity_lookup(c: Vector2i):
|
||||||
|
if _cur_settlement == null or _cur_settlement.minesweeper.current_board() == null:
|
||||||
|
return null
|
||||||
|
if String(_cur_settlement.action_economy.phase) != ActionEconomy.PHASE_ACTIVE:
|
||||||
|
return null
|
||||||
|
return _cur_settlement.query_reward_identity(c)
|
||||||
|
|
||||||
|
|
||||||
|
# 双帧逐像素比对(迭代 30 卡① T2-④-①②):所有变化像素必须落在生效格的内缘带内
|
||||||
|
# (de < cell÷8——带外含砖心零侵入=禁整格染色/禁越圈);每个生效格须有实绘像素(不空转)。
|
||||||
|
func _check_dual_frames(tag: String, view, glow_cells: Dictionary, off_img: Image, on_img: Image) -> void:
|
||||||
|
var board = view.board()
|
||||||
|
if board == null:
|
||||||
|
_problems.append("%s 双帧比对取板失败(view.board() 为 null)" % tag)
|
||||||
|
return
|
||||||
|
var w: int = off_img.get_width()
|
||||||
|
var h: int = off_img.get_height()
|
||||||
|
var cell: float = view.cell_size
|
||||||
|
var band_px: float = cell * 0.125
|
||||||
|
# 逐格窗口矩形 + 到格缘距离判据(与 type_glow_shot_probe 同口径)
|
||||||
|
var rects := {}
|
||||||
|
for gy in range(board.height):
|
||||||
|
for gx in range(board.width):
|
||||||
|
var c := Vector2i(gx, gy)
|
||||||
|
rects[c] = view.cell_rect_probe(c)
|
||||||
|
var changed_total := 0
|
||||||
|
var changed_outside := 0
|
||||||
|
var first_outside := ""
|
||||||
|
var glow_hits := {}
|
||||||
|
for c in glow_cells:
|
||||||
|
glow_hits[c] = 0
|
||||||
|
for gy in range(board.height):
|
||||||
|
for gx in range(board.width):
|
||||||
|
var c2 := Vector2i(gx, gy)
|
||||||
|
var r: Rect2 = rects[c2]
|
||||||
|
if r.size.x <= 0.0:
|
||||||
|
continue
|
||||||
|
var is_glow: bool = glow_cells.has(c2)
|
||||||
|
var x0: int = int(floor(r.position.x))
|
||||||
|
var y0: int = int(floor(r.position.y))
|
||||||
|
var x1: int = int(ceil(r.position.x + r.size.x))
|
||||||
|
var y1: int = int(ceil(r.position.y + r.size.y))
|
||||||
|
for yy in range(maxi(0, y0), mini(h, y1)):
|
||||||
|
for xx in range(maxi(0, x0), mini(w, x1)):
|
||||||
|
var de: int = mini(mini(xx - x0, x1 - 1 - xx), mini(yy - y0, y1 - 1 - yy))
|
||||||
|
var p1: Color = off_img.get_pixel(xx, yy)
|
||||||
|
var p2: Color = on_img.get_pixel(xx, yy)
|
||||||
|
if p1.r == p2.r and p1.g == p2.g and p1.b == p2.b and p1.a == p2.a:
|
||||||
|
continue
|
||||||
|
changed_total += 1
|
||||||
|
if is_glow:
|
||||||
|
if float(de) < band_px:
|
||||||
|
glow_hits[c2] = int(glow_hits[c2]) + 1
|
||||||
|
else:
|
||||||
|
changed_outside += 1
|
||||||
|
if first_outside == "":
|
||||||
|
first_outside = "%s 带外(de=%d≥%.0f)" % [str(c2), de, band_px]
|
||||||
|
else:
|
||||||
|
changed_outside += 1
|
||||||
|
if first_outside == "":
|
||||||
|
first_outside = "%s 非生效格" % str(c2)
|
||||||
|
_level_readings[tag] = {"glow_cells": glow_cells.size(), "changed_total": changed_total,
|
||||||
|
"changed_outside": changed_outside, "band_px": band_px}
|
||||||
|
print("[board_shot] %s 双帧读数 glow=%d changed=%d outside=%d band=%.0fpx"
|
||||||
|
% [tag, glow_cells.size(), changed_total, changed_outside, band_px])
|
||||||
|
if changed_outside != 0:
|
||||||
|
_problems.append("%s 非生效格/带外像素变化 %d 个(首例 %s——禁整格染色/禁越圈)" % [tag, changed_outside, first_outside])
|
||||||
|
if glow_cells.size() > 0 and changed_total == 0:
|
||||||
|
_problems.append("%s 微光开帧整板零变化(微光未生效)" % tag)
|
||||||
|
for c in glow_cells:
|
||||||
|
if int(glow_hits[c]) == 0:
|
||||||
|
_problems.append("%s 生效格 %s 零实绘像素(微光空转)" % [tag, str(c)])
|
||||||
@@ -0,0 +1,261 @@
|
|||||||
|
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||||
|
# 未开格类型微光渲染取证探针(迭代 30 卡① T2 像素侧——V4 禁侵/V5 不压破/V6 五组互辨)。
|
||||||
|
# 必须**窗口化**运行(headless 无渲染面,抓帧恒空):
|
||||||
|
# Godot_console.exe --path . res://smoke/type_glow_shot_probe.tscn -- <输出目录>
|
||||||
|
# 流程:起一板(真实 LevelRunner + settle_batch 驱动 18 步 novice——自然揭示数字/旗)→ 注入夹具身份回调
|
||||||
|
# (九 cell_type 循环 ⇒ 五组同屏齐现)→ BoardView 真渲染:
|
||||||
|
# 帧 A = 微光关(基线态)→ type_glow_off.png
|
||||||
|
# 帧 B = 微光开(生产默认态)→ type_glow_on.png
|
||||||
|
# 逐像素比对两帧:① 所有变化像素必须落在生效格的内缘带内(非生效格零变化=禁整格染色/禁越圈)
|
||||||
|
# ② 五组带内取样两两灰度对比对 CR ≥ GRAY_CR_MIN(色弱互辨通道)。
|
||||||
|
# 零存档触碰(无 save_manager 引用)。取证件落调用方给的目录(库外),不入 git。
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
const AutoPlayer := preload("res://smoke/auto_player.gd")
|
||||||
|
|
||||||
|
# 夹具:九 cell_type 全值域(地图战斗资源九型枚举同源)
|
||||||
|
const FIXTURE_TYPES := ["attack", "defense", "blast", "heal", "weaken",
|
||||||
|
"pierce", "magic", "action", "gp"]
|
||||||
|
# 8 邻方位 → 九型确定性指派(V6 五组同屏齐现的取证前提——组间互辨要求一帧内五组俱在)。
|
||||||
|
# 方位序:NW N NE W E SW S SE;探针起板后 N/NE/E 三位被 novice 挖开(恒定 seed 确定性),
|
||||||
|
# 故五组各占一个存活方位:NW=BUFF/W=PIERCE/SW=AOE/S=MAGIC/SE=HEAL,其余三位补 BUFF。
|
||||||
|
# 同屏组数不足 5 = 取证失败(见 _report 硬断言)——幸存集若变,探针报红而非静默少测。
|
||||||
|
const FIXTURE_DIR_TYPES := ["attack", "gp", "defense", "pierce",
|
||||||
|
"weaken", "blast", "magic", "heal"]
|
||||||
|
# 判据阈值(provisional——首版取严;实现报告登记,复核转正归战斗一屏样张批)
|
||||||
|
const CORE_PAD_FRAC := 0.20
|
||||||
|
const GRAY_CR_MIN := 1.20
|
||||||
|
|
||||||
|
var _hero_pos := Vector2i.ZERO
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
if DisplayServer.get_name() == "headless":
|
||||||
|
print("[type_glow_shot] FAIL — 本探针必须窗口化运行(headless 无渲染面,截图必空)")
|
||||||
|
get_tree().quit(1)
|
||||||
|
return
|
||||||
|
var args := OS.get_cmdline_user_args()
|
||||||
|
var out := args[0] if args.size() >= 1 and args[0] != "" else "user://type-glow-shots"
|
||||||
|
DirAccess.make_dir_recursive_absolute(out)
|
||||||
|
await get_tree().process_frame
|
||||||
|
var db = load("res://core/config_db.gd").new()
|
||||||
|
if not db.load_tables("res://data/tables", true):
|
||||||
|
push_error("[type_glow_shot] 表加载失败")
|
||||||
|
get_tree().quit(1)
|
||||||
|
return
|
||||||
|
var rng = load("res://core/rng_service.gd").new()
|
||||||
|
rng.set_master_seed(421001)
|
||||||
|
var runner = load("res://core/level_runner.gd").new(db, rng)
|
||||||
|
if not runner.start_level(1, 421001, {}, 0):
|
||||||
|
push_error("[type_glow_shot] 开局失败")
|
||||||
|
get_tree().quit(1)
|
||||||
|
return
|
||||||
|
var settlement = runner.current_battle()
|
||||||
|
for i in range(18):
|
||||||
|
var act: Dictionary = AutoPlayer.novice_action(settlement.minesweeper)
|
||||||
|
var kind := String(act.get("kind", "stop"))
|
||||||
|
if kind == "stop":
|
||||||
|
break
|
||||||
|
var op := kind
|
||||||
|
if kind == "flag":
|
||||||
|
op = "flag_place"
|
||||||
|
settlement.minesweeper.settle_batch([{"action": op, "cell": act.get("cell")}])
|
||||||
|
var view = load("res://presentation/views/board_view.gd").new()
|
||||||
|
view.size = Vector2(900, 900)
|
||||||
|
view.position = Vector2(40, 40)
|
||||||
|
add_child(view)
|
||||||
|
# 冻结接触带呼吸(减动效路径 motion_scale<1.0 ⇒ _apply_contact_bands 施静态色):
|
||||||
|
# 两帧比对要隔离「类型微光开关」这一单一变量,英雄格接触带相位漂移会污染非生效格零变化判据。
|
||||||
|
# 必须在 rebuild/set_dig_glow_enabled 之前置位——那两处会按当时相位重施带色。
|
||||||
|
UiKit.motion_scale = 0.5
|
||||||
|
var ui_font: Font = load("res://presentation/views/ui_kit.gd").load_font(load("res://presentation/views/ui_kit.gd").FONT_UI)
|
||||||
|
if ui_font != null:
|
||||||
|
view.set_world_font(ui_font)
|
||||||
|
# 夹具身份回调(返回形状与 query_identity 同构;8 邻按方位确定性指派,五组同屏齐现)
|
||||||
|
view.set_identity_resolver(_fixture_identity)
|
||||||
|
_hero_pos = settlement.minesweeper.hero_pos()
|
||||||
|
view.rebuild(settlement.minesweeper.current_board(), settlement.minesweeper)
|
||||||
|
# 挖掘相位:接触带呼吸与类型微光同源门控(_dig_glow);此处显式开以承取证
|
||||||
|
view.set_dig_glow_enabled(true)
|
||||||
|
# 作用集快照必须在关开关之前取(set_type_glow_enabled(false) 会清空 _glow 重算)
|
||||||
|
var glow_snapshot: Dictionary = view.type_glow_probe()
|
||||||
|
# 抓帧前等入场动画完结(board_shot_probe 同款时间制 1.2s——锚定动画完结态)
|
||||||
|
await get_tree().create_timer(1.2).timeout
|
||||||
|
await get_tree().process_frame
|
||||||
|
await RenderingServer.frame_post_draw
|
||||||
|
var on_img := get_viewport().get_texture().get_image()
|
||||||
|
if on_img == null or on_img.is_empty():
|
||||||
|
push_error("[type_glow_shot] FAIL — 抓帧空纹理(取证失败=闸红)")
|
||||||
|
get_tree().quit(1)
|
||||||
|
return
|
||||||
|
on_img.save_png(out.path_join("type_glow_on.png"))
|
||||||
|
view.set_type_glow_enabled(false)
|
||||||
|
await get_tree().process_frame
|
||||||
|
await RenderingServer.frame_post_draw
|
||||||
|
var off_img := get_viewport().get_texture().get_image()
|
||||||
|
if off_img == null or off_img.is_empty():
|
||||||
|
push_error("[type_glow_shot] FAIL — 抓帧空纹理(取证失败=闸红)")
|
||||||
|
get_tree().quit(1)
|
||||||
|
return
|
||||||
|
off_img.save_png(out.path_join("type_glow_off.png"))
|
||||||
|
_report(view, glow_snapshot, off_img, on_img, out)
|
||||||
|
|
||||||
|
|
||||||
|
# 逐像素比对与读数:V4(非生效格零变化+砖心零侵入)/V5(灰度分类一致)/V6(五组互辨)
|
||||||
|
# glow_snapshot = 关开关前的作用集快照(set_type_glow_enabled(false) 会清空 _glow 重算,故必须外传)
|
||||||
|
func _report(view, glow_snapshot: Dictionary, off_img: Image, on_img: Image, out: String) -> void:
|
||||||
|
var glow_cells: Dictionary = glow_snapshot.get("cells", {})
|
||||||
|
var bad: Array[String] = []
|
||||||
|
var readings := {}
|
||||||
|
if not bool(glow_snapshot.get("enabled", false)):
|
||||||
|
bad.append("微光开关态非默认开")
|
||||||
|
if glow_cells.is_empty():
|
||||||
|
bad.append("微光作用集为空(英雄 8 邻未开格未派生出组号)")
|
||||||
|
var board = view.board()
|
||||||
|
var w: int = off_img.get_width()
|
||||||
|
var h: int = off_img.get_height()
|
||||||
|
var cell: float = view.cell_size
|
||||||
|
var band_px: float = cell * 0.125
|
||||||
|
var core_pad: float = cell * CORE_PAD_FRAC
|
||||||
|
var changed_total := 0
|
||||||
|
var changed_outside := 0
|
||||||
|
var by_group := {}
|
||||||
|
if board != null:
|
||||||
|
for gy in range(board.height):
|
||||||
|
for gx in range(board.width):
|
||||||
|
var c := Vector2i(gx, gy)
|
||||||
|
var r: Rect2 = view.cell_rect_probe(c)
|
||||||
|
if r.size.x <= 0.0:
|
||||||
|
continue
|
||||||
|
var is_glow: bool = glow_cells.has(c)
|
||||||
|
var g: int = int(glow_cells[c]) if is_glow else 0
|
||||||
|
var x0: int = int(floor(r.position.x))
|
||||||
|
var y0: int = int(floor(r.position.y))
|
||||||
|
var x1: int = int(ceil(r.position.x + r.size.x))
|
||||||
|
var y1: int = int(ceil(r.position.y + r.size.y))
|
||||||
|
var gsum := 0.0
|
||||||
|
var gn := 0
|
||||||
|
for yy in range(maxi(0, y0), mini(h, y1)):
|
||||||
|
for xx in range(maxi(0, x0), mini(w, x1)):
|
||||||
|
var de: int = mini(mini(xx - x0, x1 - 1 - xx), mini(yy - y0, y1 - 1 - yy))
|
||||||
|
var p1: Color = off_img.get_pixel(xx, yy)
|
||||||
|
var p2: Color = on_img.get_pixel(xx, yy)
|
||||||
|
if is_glow and float(de) < band_px:
|
||||||
|
gsum += _lum(p2)
|
||||||
|
gn += 1
|
||||||
|
if p1.r == p2.r and p1.g == p2.g and p1.b == p2.b and p1.a == p2.a:
|
||||||
|
continue
|
||||||
|
changed_total += 1
|
||||||
|
if (not is_glow) or float(de) >= core_pad - 1.0:
|
||||||
|
changed_outside += 1
|
||||||
|
if is_glow and gn > 0 and not by_group.has(g):
|
||||||
|
by_group[g] = gsum / float(gn)
|
||||||
|
readings["glow_cells"] = glow_cells.size()
|
||||||
|
readings["groups"] = _sorted_keys(by_group)
|
||||||
|
readings["changed_total"] = changed_total
|
||||||
|
readings["changed_outside"] = changed_outside
|
||||||
|
readings["band_lum"] = by_group
|
||||||
|
var rects := {}
|
||||||
|
for c in glow_cells:
|
||||||
|
var rr: Rect2 = view.cell_rect_probe(c)
|
||||||
|
rects[str(c)] = [rr.position.x, rr.position.y, rr.size.x, rr.size.y]
|
||||||
|
readings["rects"] = rects
|
||||||
|
if changed_total == 0:
|
||||||
|
bad.append("开微光后整帧零像素变化(微光未生效)")
|
||||||
|
if changed_outside != 0:
|
||||||
|
bad.append("非生效格像素变化 %d 个(禁整格染色/禁越圈)" % changed_outside)
|
||||||
|
if by_group.size() < 5:
|
||||||
|
bad.append("同屏组数 %d < 5(V6 五组并列互辨的取证前提不成立)" % by_group.size())
|
||||||
|
var ks: Array = _sorted_keys(by_group)
|
||||||
|
var pairs: Array[String] = []
|
||||||
|
var worst_cr := 99.0
|
||||||
|
var worst_pair := ""
|
||||||
|
for i in range(ks.size()):
|
||||||
|
for j in range(i + 1, ks.size()):
|
||||||
|
# WCAG 相对亮度对比对:band 均值先经 sRGB→线性反伽马(+0.05 偏移定义在线性域),
|
||||||
|
# 与 tools/shot_checker.py 的 WCAG 段同口径(色弱互辨通道的机检同款公式)。
|
||||||
|
var la: float = _to_linear(float(by_group[ks[i]]))
|
||||||
|
var lb: float = _to_linear(float(by_group[ks[j]]))
|
||||||
|
var cr: float = (maxf(la, lb) + 0.05) / (minf(la, lb) + 0.05)
|
||||||
|
pairs.append("组%d/组%d CR=%.3f ΔL*=%.1f" % [ks[i], ks[j], cr, absf(_lstar(la) - _lstar(lb))])
|
||||||
|
if cr < worst_cr:
|
||||||
|
worst_cr = cr
|
||||||
|
worst_pair = "组%d/组%d" % [ks[i], ks[j]]
|
||||||
|
readings["pairs"] = " ".join(pairs)
|
||||||
|
readings["worst_cr"] = worst_cr
|
||||||
|
readings["worst_pair"] = worst_pair
|
||||||
|
if ks.size() >= 2 and worst_cr < GRAY_CR_MIN:
|
||||||
|
bad.append("灰度互辨最弱对 %s CR=%.3f < %.3f(色弱通道不可分)" % [worst_pair, worst_cr, GRAY_CR_MIN])
|
||||||
|
var mf := FileAccess.open(out.path_join("type_glow_readings.json"), FileAccess.WRITE)
|
||||||
|
if mf != null:
|
||||||
|
mf.store_string(JSON.stringify(readings, " "))
|
||||||
|
mf.close()
|
||||||
|
print("[type_glow_shot] 读数 %s" % JSON.stringify(readings))
|
||||||
|
if bad.is_empty():
|
||||||
|
print("[type_glow_shot] PASS — 非生效格零变化/砖心零侵入/五组灰度互辨全过")
|
||||||
|
get_tree().quit(0)
|
||||||
|
else:
|
||||||
|
for m in bad:
|
||||||
|
print("[type_glow_shot] FAIL — %s" % m)
|
||||||
|
get_tree().quit(1)
|
||||||
|
|
||||||
|
|
||||||
|
# 夹具身份回调(形状与 query_identity 返回同构;8 邻按方位取九型之一,其余格按坐标兜底)
|
||||||
|
func _fixture_identity(c: Vector2i) -> Variant:
|
||||||
|
var ctype: String
|
||||||
|
var di: int = _dir_index(c - _hero_pos)
|
||||||
|
if di >= 0:
|
||||||
|
ctype = FIXTURE_DIR_TYPES[di]
|
||||||
|
else:
|
||||||
|
ctype = FIXTURE_TYPES[int(abs(c.x * 31 + c.y * 17)) % FIXTURE_TYPES.size()]
|
||||||
|
return {"reward_id": "fix_%d_%d" % [c.x, c.y], "cell_type": ctype, "tier": 1}
|
||||||
|
|
||||||
|
|
||||||
|
# 方位偏移 → 0..7(NW N NE W E SW S SE;非 8 邻返回 -1)
|
||||||
|
static func _dir_index(off: Vector2i) -> int:
|
||||||
|
match off:
|
||||||
|
Vector2i(-1, -1):
|
||||||
|
return 0
|
||||||
|
Vector2i(0, -1):
|
||||||
|
return 1
|
||||||
|
Vector2i(1, -1):
|
||||||
|
return 2
|
||||||
|
Vector2i(-1, 0):
|
||||||
|
return 3
|
||||||
|
Vector2i(1, 0):
|
||||||
|
return 4
|
||||||
|
Vector2i(-1, 1):
|
||||||
|
return 5
|
||||||
|
Vector2i(0, 1):
|
||||||
|
return 6
|
||||||
|
Vector2i(1, 1):
|
||||||
|
return 7
|
||||||
|
return -1
|
||||||
|
|
||||||
|
|
||||||
|
static func _sorted_keys(d: Dictionary) -> Array:
|
||||||
|
var ks: Array = d.keys()
|
||||||
|
ks.sort()
|
||||||
|
return ks
|
||||||
|
|
||||||
|
|
||||||
|
# 相对亮度(Rec.709 系数——灰度互辨通道与 shot_checker 同口径)
|
||||||
|
# 相对亮度(Rec.709 系数——灰度互辨通道与 shot_checker 同口径;输入为帧缓冲 sRGB 值)
|
||||||
|
static func _lum(c: Color) -> float:
|
||||||
|
return 0.2126 * c.r + 0.7152 * c.g + 0.0722 * c.b
|
||||||
|
|
||||||
|
|
||||||
|
# sRGB(伽马)值 → 线性相对亮度(WCAG 2.x 反伽马——对比对公式的输入域)
|
||||||
|
static func _to_linear(y_srgb: float) -> float:
|
||||||
|
var c: float = clampf(y_srgb, 0.0, 1.0)
|
||||||
|
if c <= 0.04045:
|
||||||
|
return c / 12.92
|
||||||
|
return pow((c + 0.055) / 1.055, 2.4)
|
||||||
|
|
||||||
|
|
||||||
|
# CIE L*(Y∈[0,1] → L*∈[0,100];ΔL* 读数供互辨裕量评估)
|
||||||
|
static func _lstar(y: float) -> float:
|
||||||
|
var yy: float = clampf(y, 0.0, 1.0)
|
||||||
|
if yy <= 0.008856:
|
||||||
|
return 903.3 * yy
|
||||||
|
return 116.0 * pow(yy, 1.0 / 3.0) - 16.0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://djmrucr7nxl5j
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://smoke/type_glow_shot_probe.gd" id="1"]
|
||||||
|
|
||||||
|
[node name="TypeGlowShotProbe" type="Node"]
|
||||||
|
script = ExtResource("1")
|
||||||
Reference in new issue
Block a user