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——取证段失败=闸红,
|
||||
# 零字节/空纹理不允许静默落盘;board_meta.json 取样坐标落盘(棋盘视图全局矩形——
|
||||
# golden 前置裁剪坐标源,禁硬编码像素)。
|
||||
# 迭代 30 卡① T2-④ 双帧取证:每关抓 微光开(生产默认态·Q4 丙案)/关(golden 基线保护面)
|
||||
# 两帧——① 开帧逐像素判定「非生效格零变化」(禁整格染色/禁越圈;生效格内只准落在内缘
|
||||
# 带 = cell÷8)② 每个生效格须有实绘像素(微光不空转)③ 关帧即 golden 对比帧(原名落盘,
|
||||
# 基线 2026-09-30 代不重制——契约 1 off 态基线)。隔离纪律 = 与 type_glow_shot_probe 同款:
|
||||
# 前置 UiKit.motion_scale=0.5 冻结接触带呼吸相位(静态带色),两帧差异唯一定位在类型微光。
|
||||
extends Node
|
||||
|
||||
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 _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:
|
||||
@@ -30,6 +41,11 @@ func _ready() -> void:
|
||||
push_error("[board_shot] 表加载失败")
|
||||
get_tree().quit(1)
|
||||
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 targets := [[1, "L1_dungeon"], [2, "L2_underdark"], [3, "L3_deep_underdark"]]
|
||||
for t in targets:
|
||||
@@ -43,6 +59,7 @@ func _ready() -> void:
|
||||
get_tree().quit(1)
|
||||
return
|
||||
var settlement = runner.current_battle()
|
||||
_cur_settlement = settlement
|
||||
for i in range(18):
|
||||
var act: Dictionary = AutoPlayer.novice_action(settlement.minesweeper)
|
||||
var kind := String(act.get("kind", "stop"))
|
||||
@@ -58,7 +75,18 @@ func _ready() -> void:
|
||||
if ui_font != null:
|
||||
view.set_world_font(ui_font)
|
||||
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)
|
||||
# 进挖掘相位(生产态 = 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 有全板淡入
|
||||
# motion(0.2)+R0 涟漪逐格延迟显形(motion(0.04×离板心距离)+motion(0.12),最远格 ≈0.56s),
|
||||
# 原制 6 个 process_frame 热机后 ≈0.1s=捕在涟漪中途——逐跑帧时序抖动使揭示 alpha 漂移,
|
||||
@@ -66,21 +94,124 @@ func _ready() -> void:
|
||||
await get_tree().create_timer(1.2).timeout
|
||||
await get_tree().process_frame
|
||||
await RenderingServer.frame_post_draw
|
||||
var img := get_viewport().get_texture().get_image()
|
||||
if img == null or img.is_empty():
|
||||
var on_img := get_viewport().get_texture().get_image()
|
||||
if on_img == null or on_img.is_empty():
|
||||
push_error("[board_shot] FAIL — 抓帧空纹理: %s(Q7 取证语义=闸红)" % tag)
|
||||
get_tree().quit(1)
|
||||
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]
|
||||
img.save_png(path)
|
||||
off_img.save_png(path)
|
||||
_meta[tag] = {"x": view.global_position.x, "y": view.global_position.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()
|
||||
await get_tree().process_frame
|
||||
var mf := FileAccess.open(out.path_join("board_meta.json"), FileAccess.WRITE)
|
||||
if mf != null:
|
||||
mf.store_string(JSON.stringify(_meta))
|
||||
mf.close()
|
||||
print("[board_shot] done")
|
||||
get_tree().quit(0)
|
||||
var rf := FileAccess.open(out.path_join("board_dual_frame_readings.json"), FileAccess.WRITE)
|
||||
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