218 lines
12 KiB
GDScript
218 lines
12 KiB
GDScript
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||
# 棋盘渲染帧取证探针(目视验证面:机检≠看得清)。
|
||
# 必须**窗口化**运行(headless 无渲染面):
|
||
# Godot_console.exe --path . res://probe/board_shot_probe.tscn -- <输出目录>
|
||
# 流程:L1/L2/L3 各起一板(真实 LevelRunner+settle_batch 驱动 ~18 步 novice——自然揭示数字/封印),
|
||
# BoardView 真渲染,frame_post_draw 同步后抓视口 PNG。零存档触碰(无 save_manager 引用)。
|
||
# 护栏:headless fail-fast(shot_probe 同款);空纹理取证 fail——取证段失败=闸红,
|
||
# 零字节/空纹理不允许静默落盘;board_meta.json 取样坐标落盘(棋盘视图全局矩形——
|
||
# golden 前置裁剪坐标源,禁硬编码像素)。
|
||
# 双帧取证:每关抓 微光开(生产默认态·Q4 丙案)/关(golden 基线保护面)
|
||
# 两帧——① 开帧逐像素判定「非生效格零变化」(禁整格染色/禁越圈;生效格内只准落在内缘
|
||
# 带 = cell÷8)② 每个生效格须有实绘像素(微光不空转)③ 关帧即 golden 对比帧(原名落盘,
|
||
# 基线不重制——契约 1 off 态基线)。隔离纪律 = 与 type_glow_shot_probe 同款:
|
||
# 前置 UiKit.motion_scale=0.5 冻结接触带呼吸相位(静态带色),两帧差异唯一定位在类型微光。
|
||
extends Node
|
||
|
||
const AutoPlayer := preload("res://sim/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:
|
||
# 运行前提=窗口化真渲染面(shot_probe 同款护栏——headless 下 frame_post_draw 永不触发+视口纹理恒空)
|
||
if DisplayServer.get_name() == "headless":
|
||
print("[board_shot] FAIL — 本探针必须窗口化运行(headless 无渲染面,截图必空);现行事实源=tools/render_gate.sh 取证段")
|
||
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://board-shots"
|
||
DirAccess.make_dir_recursive_absolute(out)
|
||
await get_tree().process_frame
|
||
var db = load("res://core/config_db.gd").new()
|
||
var ok: bool = db.load_tables("res://data/tables", true)
|
||
if not ok:
|
||
push_error("[board_shot] 表加载失败")
|
||
get_tree().quit(1)
|
||
return
|
||
# 双帧单一变量隔离:冻结接触带呼吸相位——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:
|
||
var level_index: int = t[0]
|
||
var tag: String = t[1]
|
||
var rng = load("res://core/rng_service.gd").new()
|
||
rng.set_master_seed(420000 + level_index)
|
||
var runner = load("res://core/level_runner.gd").new(db, rng)
|
||
if not runner.start_level(level_index, 420000 + level_index, {}, 0):
|
||
push_error("[board_shot] 开关失败 level=%d" % level_index)
|
||
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"))
|
||
if kind == "stop":
|
||
break
|
||
var op := kind
|
||
if kind == "seal":
|
||
op = "seal_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)
|
||
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 ⇒ 作用集恒空(三关 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 基线即捕获于
|
||
# 该裸态。双帧取证要求生产默认态,故显式开——两帧均含静态接触带(motion_scale=0.5 停格),
|
||
# 差异唯一定位在类型微光开关)
|
||
view.set_dig_glow_enabled(true)
|
||
# 抓帧前等入场动画完结(S2 观察项根因):rebuild 有全板淡入
|
||
# motion(0.2)+R0 涟漪逐格延迟显形(motion(0.04×离板心距离)+motion(0.12),最远格 ≈0.56s),
|
||
# 原制 6 个 process_frame 热机后 ≈0.1s=捕在涟漪中途——逐跑帧时序抖动使揭示 alpha 漂移,
|
||
# golden L2/L3 跨 2% 容差线波动(实测 0.00-2.53%)。改时间制 1.2s(2×余量)=锚定动画完结态。
|
||
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("[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]
|
||
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) + %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()
|
||
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)
|
||
|
||
|
||
# 双帧逐像素比对:所有变化像素必须落在生效格的内缘带内
|
||
# (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)])
|