# tests/unit/presentation/terrain_tile_sets_test.gd — 瓦片换色机检(T6 卡交付:色相域抽检+套完整性) # 运行:godot --headless --path game --script res://tests/unit/presentation/terrain_tile_sets_test.gd # 事实源:board_gen.tres board_type 词表 v1.1(dungeon/underdark/deep_underdark——从表读值,禁写死); # 色相域方向 = art-bible §4.3 三关方向(underdark 灰白蓝紫 / deep_underdark 蓝黑紫黑); # 图底分离 = 圣经 §8.5(已开地面明度低于未开墙体)。 # 前置守卫:素材不入 git——fresh checkout 无瓦片文件时本套件跳过(打印注记,不红); # 源 dungeon 套在盘而换色套缺位 = 回归(换色脚本在库可复现)。 extends SceneTree const ConfigDBScript := preload("res://core/config_db.gd") const TilePaletteScript := preload("res://presentation/views/tile_palette.gd") const Kit := preload("res://tests/unit/test_kit.gd") # T6 收口:_check/汇总退出样板单点 func _init() -> void: var board_dir := ProjectSettings.globalize_path("res://assets/art/board/") var probe_path := board_dir.path_join("terrain_wall_center_dungeon.png") if not FileAccess.file_exists(probe_path): print("[terrain_tile_sets_test] SKIP——素材工作区缺位(瓦片不入 git 纪律,fresh checkout 预期态)") quit(0) return var db = ConfigDBScript.new() var load_ok: bool = db.load_tables("res://data/tables", true) Kit.check("前置:表加载零 error", load_ok and db.diagnostics_count("error") == 0) if not load_ok: quit(1) return # board_type 词表自 board_gen 行集推导(禁测试侧写死词表) var types: Array = [] for row in db.combat.board_gen.rows: var bt := String(row.get("board_type", "")) if bt != "" and not types.has(bt): types.append(bt) Kit.check("前置:board_gen 词表非空(%d 型)" % types.size(), types.size() > 0) _test_set_completeness(board_dir, types) _test_hue_domains(board_dir) _test_digit_ink_modes(board_dir, types) quit(Kit.finish("terrain_tile_sets_test")) # 套完整性:词表每个 board_type 的 16 墙 + 5 地面变体全部在盘(缺位 → BoardView 整批回落 dungeon 的 # 缺位回退断言由 ui_input_probe 覆盖;本断言管「入包完整」面) func _test_set_completeness(board_dir: String, types: Array) -> void: var wall_names: Array = TilePaletteScript.TILE_WALL_NAMES var variants: int = int(TilePaletteScript.TILE_FLOOR_VARIANTS) for bt in types: var missing: Array = [] for w in wall_names: var p := board_dir.path_join("terrain_%s_%s.png" % [String(w), String(bt)]) if not FileAccess.file_exists(p): missing.append(String(w)) for i in range(1, variants + 1): var pf := board_dir.path_join("terrain_floor_%s_%d.png" % [String(bt), i]) if not FileAccess.file_exists(pf): missing.append("floor_%d" % i) Kit.check("图型 %s 瓦片套完整(16 墙 + %d 地面,缺=%s)" % [String(bt), variants, str(missing)], missing.is_empty()) # 色相域抽检:wall_center 上半幅(墙顶面)与 floor_1 全幅的均值色相/明度 func _test_hue_domains(board_dir: String) -> void: var bands := { "dungeon": [0.02, 0.16], # 暖石棕 ≈15°–58° "underdark": [0.50, 0.72], # 灰白蓝紫 ≈180°–260° "deep_underdark": [0.55, 0.78], # 蓝黑紫黑 ≈200°–280° } var wall_v := {} var floor_v := {} for bt in bands.keys(): var wall := _load_stats(board_dir, "terrain_wall_center_%s.png" % String(bt), true) var floor_img := _load_stats(board_dir, "terrain_floor_%s_1.png" % String(bt), false) if wall.is_empty() or floor_img.is_empty(): Kit.check("图型 %s 抽检样本可读" % String(bt), false) continue wall_v[bt] = float(wall["v"]) floor_v[bt] = float(floor_img["v"]) var band: Array = bands[bt] Kit.check("图型 %s 墙顶均值色相在带 %s(实测 %.3f)" % [String(bt), str(band), float(wall["h"])], float(wall["h"]) >= float(band[0]) and float(wall["h"]) <= float(band[1])) Kit.check("图型 %s 图底分离(墙顶明度 %.3f > 地面 %.3f)" % [String(bt), wall_v[bt], floor_v[bt]], wall_v[bt] > floor_v[bt]) if wall_v.has("underdark") and wall_v.has("deep_underdark"): Kit.check("deep_underdark 墙顶更暗于 underdark(更深蓝黑方向)", float(wall_v["deep_underdark"]) < float(wall_v["underdark"])) # 均值读数:hue 取饱和度 > 0.05 的像素(墨线/灰黑不影响色相均值);v 取全幅均值 func _load_stats(board_dir: String, fname: String, top_half: bool) -> Dictionary: var p := board_dir.path_join(fname) if not FileAccess.file_exists(p): return {} var img := Image.load_from_file(p) if img == null: return {} if img.is_compressed(): img.decompress() var w := img.get_width() var h := img.get_height() var y0 := 0 var y1 := h if top_half: y1 = h / 2 # 墙顶面 = 上半幅(立面暗带不参与色相均值) var h_sum := 0.0 var v_sum := 0.0 var n_h := 0 var n_v := 0 for y in range(y0, y1): for x in range(w): var c := img.get_pixel(x, y) v_sum += c.v n_v += 1 if c.s > 0.05: h_sum += c.h n_h += 1 if n_v == 0 or n_h == 0: return {} return {"h": h_sum / n_h, "v": v_sum / n_v} # 数字呈现模式对比断言(L2/L3 亮瓦融字缺陷的甲案机检面,v3.4 扩三态): # 每个图型走 BoardView 同一归一+测光+选模式管线(禁测试侧另写算法),断言: # ①dungeon=亮墨族直画(对 p90 ≥4.5 且对均值 ≥2.5);②underdark/deep_underdark=色牌垫底 # (双峰纹理直画不可救——渲染帧取证深墨隐形实证,牌上深墨 12:1);③直画模式的双门槛值照抄 # TilePalette 判据常量(防判据漂移) func _test_digit_ink_modes(board_dir: String, types: Array) -> void: var expect_plate := {"dungeon": false, "underdark": true, "deep_underdark": true} var variants: int = int(TilePaletteScript.TILE_FLOOR_VARIANTS) for bt in types: var lumas: Array = [] var readable := true for i in range(1, variants + 1): var p := board_dir.path_join("terrain_floor_%s_%d.png" % [String(bt), i]) if not FileAccess.file_exists(p): readable = false break var tex: Texture2D = ImageTexture.create_from_image(Image.load_from_file(p)) TilePaletteScript.normalize_floor_luma(tex, 0.10, lumas) if not readable: Kit.check("图型 %s 数字墨族断言样本可读" % String(bt), false) continue var mode: Dictionary = TilePaletteScript.digit_ink_mode(lumas) var plate: bool = bool(mode["plate"]) var dark: bool = bool(mode["dark"]) var mode_name := "色牌垫底" if plate else ("深墨族直画" if dark else "亮墨族直画") if plate: Kit.check("图型 %s 双峰纹理落牌(亮族 p90 %.2f/均值 %.2f,深族 p90 %.2f/均值 %.2f 均未过门)" % [ String(bt), float(mode["cr_p90_bright"]), float(mode["cr_mean_bright"]), float(mode["cr_p90_dark"]), float(mode["cr_mean_dark"])], float(mode["cr_mean_dark"]) < float(TilePaletteScript.DIGIT_INK_CR_MEAN_DARK)) else: var cr_p90 := float(mode["cr_p90_bright"]) if not dark else float(mode["cr_p90_dark"]) var cr_mean := float(mode["cr_mean_bright"]) if not dark else float(mode["cr_mean_dark"]) Kit.check("图型 %s 所选%s 对 p90 ≥%.1f(实测 %.2f)" % [String(bt), mode_name, float(TilePaletteScript.DIGIT_INK_CR_P90), cr_p90], cr_p90 >= float(TilePaletteScript.DIGIT_INK_CR_P90)) var mean_gate := float(TilePaletteScript.DIGIT_INK_CR_MEAN_BRIGHT) if not dark else float(TilePaletteScript.DIGIT_INK_CR_MEAN_DARK) Kit.check("图型 %s 所选%s 对均值 ≥%.1f(实测 %.2f)" % [String(bt), mode_name, mean_gate, cr_mean], cr_mean >= mean_gate) if expect_plate.has(bt): Kit.check("图型 %s 呈现模式期望=%s(实测=%s)" % [String(bt), "色牌" if bool(expect_plate[bt]) else "直画", mode_name], plate == bool(expect_plate[bt]))