test(断言三件): 未开格类型微光机检面(迭代 30 卡① T2-①②③)——①冒烟 smoke_all.gd 新增 _smoke_type_glow 八相 24 项(相0 作用集恒等+默认开+已开/r0 零微光/相1 八邻派八型五组逐格映射/相2 清单外"keystone"显式登记+零微光/相3 封锁格零微光零登记/相4 解封复位/相5 off/on 两态+关前快照逐位复现+零玩法写入三面/相6 换图 rebuild 重算/相7 契约 1 同板同序列 on/off 双实例结算逐位一致 12 步 digest 对照);夹具 = seed 4242 L1 板+novice 驱动(初始展开覆盖 8 邻故必驱动)+八邻派型+哈希兜底九型全集;冒烟 1127→1151 项。②单测 type_glow_test.gd 五段 51 检查(归组封闭含清单外 6 负样本+两带并存锚定/冻结色相面 HSV 稽核 ΔH≤0.2°·ΔS≤0.01+群伤 V=0.65 专项/呈现参数锚定 α170·band 0.125·组号域 0-5/伽马合成空间阶梯单调+相邻组模型 CR≥1.20/probe API 形状),登记 run_all.gd(33→34 套件);extends SceneTree+Kit/UiKitScript preload(--script 模式禁 class_name 裸引用)。③ui_input_probe case⑰ 活板面走查(生产身份面:作用集==diggable 键集/真实点击挖开即灭/移动复亮出圈灭/相位门控两态/开关两态零扰动),十八断言全绿。批末变异验证五组反例实证:M1 归组错映(单测+冒烟红)/M2 清单外静默(冒烟红)/M3 相位切换不重算(冒烟+探针红)/M4 α=0 不绘(渲染闸红)/M5 派生路径误触玩法 API(冒烟相7 分叉红)——红输出与还原结论实录见 reports/iter30-实现报告-2026-10-17.md §四。

This commit is contained in:
DKChan committed 2026-10-17 10:18:35 +08:00
1 parent bd44e658e8
commit 80ac80ef1a
5 files changed
+544 -1

No files matched your search

+268
View File
@@ -651,6 +651,7 @@ func _run_core_smoke(db, rng) -> void:
_smoke_pool_wiring()
_smoke_board_generator()
_smoke_layout_cell_formula()
_smoke_type_glow()
_smoke_variation_framework()
_smoke_variant_placement(1)
_smoke_variant_placement(2)
@@ -1693,6 +1694,273 @@ func _smoke_layout_cell_formula() -> void:
board_view.free()
# ---- 未开格类型微光·数据侧断言(迭代 30 卡① T2-①——V1 作用集恒等/V2 九型归组封闭/V7 零玩法写入)----
# 夹具纪律:身份回调形状与 reward_economy.query_identity 同构({reward_id, cell_type, tier});
# 位点覆盖表 _glow_type_overrides(Vector2i → cell_type)+封锁集 _glow_blocked(Vector2i → true,
# 返回 null = 不在摇号清单的封锁格)。哈希兜底按坐标确定性散列——全缸可复现。
const _GLOW_FIXTURE_TYPES := ["attack", "defense", "blast", "heal", "weaken",
"pierce", "magic", "action", "gp"]
var _glow_type_overrides := {}
var _glow_blocked := {}
func _glow_fixture_identity(c: Vector2i) -> Variant:
if _glow_blocked.has(c):
return null
var ctype := String(_glow_type_overrides.get(c, ""))
if ctype == "":
ctype = _GLOW_FIXTURE_TYPES[int(abs(c.x * 31 + c.y * 17)) % _GLOW_FIXTURE_TYPES.size()]
return {"reward_id": "fix_%d_%d" % [c.x, c.y], "cell_type": ctype, "tier": 1}
# 英雄 8 邻中未开且未移除的格数(夹具起板前提——初始展开常把 8 邻全揭示)
func _glow_unrevealed_neighbors(ms) -> int:
var n := 0
var hero: Vector2i = ms.hero_pos()
for c in ms.current_board().neighbors8(hero):
if not ms.is_revealed(c) and not ms.is_removed(c):
n += 1
return n
# 独立重算的期望作用集(不读 BoardView 私有域——恒等断言的两侧各自算)
func _glow_expected_set(b, ms) -> Dictionary:
var out := {}
var hero: Vector2i = ms.hero_pos()
for n in b.neighbors8(hero):
if not ms.is_revealed(n) and not ms.is_removed(n):
out[n] = true
return out
# 键集相等(期望集是纯键集——V1 恒等面对照)
func _glow_key_set_equal(a: Dictionary, b: Dictionary) -> bool:
if a.size() != b.size():
return false
for k in a:
if not b.has(k):
return false
return true
# 键值逐位相等(off→on 复现——两侧都是作用集快照,组号亦须逐位一致)
func _glow_map_equal(a: Dictionary, b: Dictionary) -> bool:
if a.size() != b.size():
return false
for k in a:
if not b.has(k) or int(a[k]) != int(b[k]):
return false
return true
func _smoke_type_glow() -> void:
print("--- 未开格类型微光(作用集恒等/九型归组封闭/零玩法写入)---")
var board_generator = load("res://core/board_generator.gd").new()
var b = board_generator.generate(1, 0, _mk_stream("board_gen", 4242), _db)
var ae = load("res://core/action_economy.gd").new(_db)
var ms = load("res://core/minesweeper_rules.gd").new(_db, ae)
ms.install_board(b)
ae.notify_round_start()
# 新手策略驱动到「英雄邻域有未开格」态(初始展开常把 8 邻全揭示——取证探针同款起手法;
# 恒定种子确定性,步数上限 40 防冻态空转)
var steps := 0
while _glow_unrevealed_neighbors(ms) < 5 and steps < 40:
var act: Dictionary = AutoPlayer.novice_action(ms)
if String(act.get("kind", "stop")) == "stop":
break
var op := String(act.get("kind", ""))
if op == "flag":
op = "flag_place"
ms.settle_batch([{"action": op, "cell": act.get("cell")}])
steps += 1
var view = load("res://presentation/views/board_view.gd").new()
view.set_identity_resolver(_glow_fixture_identity)
view.rebuild(b, ms)
view.set_dig_glow_enabled(true)
var hero: Vector2i = ms.hero_pos()
# 快照 trio:棋盘态(零玩法写入的对照面)+作用集
var revealed_before: Array = ms.revealed_cells()
revealed_before.sort()
var hero_before: Vector2i = ms.hero_pos()
var flags_before: int = ms.flag_count()
# ---- 相 0:夹具全集合法九型 ⇒ 作用集 == 独立重算期望集(V1)----
var snap: Dictionary = view.type_glow_probe()
check(bool(snap.get("enabled", false)), "V1 微光默认开(Q4 丙案——呈现态布尔缺省 true,生产写入面零显式开)")
check((snap.get("errors", []) as Array).is_empty(), "V1 夹具全集合法九型 ⇒ 清单外登记为空(禁静默空光通道零误报)")
var expect: Dictionary = _glow_expected_set(b, ms)
check(_glow_key_set_equal(snap.get("cells", {}), expect),
"V1 作用集恒等:微光格集合 == _diggable ∩ 身份可查(期望 %d,实测 %d)" % [expect.size(), (snap.get("cells", {}) as Dictionary).size()])
check(expect.size() >= 5, "V1 夹具前提:英雄未开邻格 ≥5(实测 %d——novice %d 步)" % [expect.size(), steps])
# 已开格/r0 出生展开区零微光(S1:已开格不显示)
var opened_leak := ""
for c in ms.revealed_cells():
if (snap.get("cells", {}) as Dictionary).has(c):
opened_leak += str(c) + " "
for c in b.compute_r0():
if (snap.get("cells", {}) as Dictionary).has(c):
opened_leak += "r0:" + str(c) + " "
check(opened_leak == "", "V1 已开格/r0 出生展开区零微光(S1)%s" % ("" if opened_leak == "" else "违例:" + opened_leak))
# ---- 相 1:八邻派定八型 ⇒ 五组逐格映射(V2 活板面)----
var nbs: Array = b.neighbors8(hero)
var assign := ["attack", "heal", "pierce", "blast", "magic", "defense", "action", "gp"]
var expect_groups := {"attack": UiKit.GLOW_BUFF, "defense": UiKit.GLOW_BUFF, "weaken": UiKit.GLOW_BUFF,
"action": UiKit.GLOW_BUFF, "gp": UiKit.GLOW_BUFF, "heal": UiKit.GLOW_HEAL,
"pierce": UiKit.GLOW_PIERCE, "blast": UiKit.GLOW_AOE, "magic": UiKit.GLOW_MAGIC}
var mapped := 0
for i in mini(assign.size(), nbs.size()):
var nb: Vector2i = nbs[i]
if ms.is_revealed(nb) or ms.is_removed(nb):
continue # 已开邻格不派定——只测未开格
_glow_type_overrides[nb] = assign[i]
mapped += 1
view.refresh()
snap = view.type_glow_probe()
check(mapped >= 5, "V2 夹具前提:未开邻格派定 ≥5 型(实测 %d)" % mapped)
var map_bad := ""
for i in mini(assign.size(), nbs.size()):
var nb2: Vector2i = nbs[i]
if not _glow_type_overrides.has(nb2):
continue
var got: int = int((snap.get("cells", {}) as Dictionary).get(nb2, 0))
if got != int(expect_groups[assign[i]]):
map_bad += "%s:%s→组%d(期望组%d) " % [str(nb2), assign[i], got, int(expect_groups[assign[i]])]
check(map_bad == "", "V2 九型→五组逐格映射(attack/defense/action/gp→增益/heal→治疗/pierce→穿刺/blast→群伤/magic→魔法)%s"
% ("" if map_bad == "" else "违例:" + map_bad))
# ---- 相 2:清单外 cell_type ⇒ 显式登记 + 该格无微光(V2 禁静默空光)----
var bogus_cell: Vector2i = hero
for i in mini(assign.size(), nbs.size()):
var nb3: Vector2i = nbs[i]
if _glow_type_overrides.has(nb3):
bogus_cell = nb3
break
_glow_type_overrides[bogus_cell] = "keystone" # 大宝箱未来第十型之外的清单外值
view.refresh()
snap = view.type_glow_probe()
var errs: Array = snap.get("errors", [])
check(errs.has("keystone"), "V2 清单外 cell_type 显式登记(errors=%s)" % str(errs))
check(not (snap.get("cells", {}) as Dictionary).has(bogus_cell),
"V2 清单外格零微光(无静默空光——%s 不在作用集)" % str(bogus_cell))
check((snap.get("cells", {}) as Dictionary).size() == mapped - 1,
"V2 清单外格剔除后作用集 = 派定数 −1(%d→%d)" % [mapped, (snap.get("cells", {}) as Dictionary).size()])
# ---- 相 3:封锁格(query 返回 null)⇒ 无微光且零登记 ----
_glow_type_overrides.erase(bogus_cell) # 复位清单外(errors 保留=板级登记语义)
var blocked_cell: Vector2i = hero
for i in mini(assign.size(), nbs.size()):
var nb4: Vector2i = nbs[i]
if _glow_type_overrides.has(nb4):
blocked_cell = nb4
break
_glow_blocked[blocked_cell] = true
view.refresh()
snap = view.type_glow_probe()
check(not (snap.get("cells", {}) as Dictionary).has(blocked_cell),
"V1 封锁格零微光(query null ⇒ 天然惰性——%s)" % str(blocked_cell))
check((snap.get("errors", []) as Array).size() == 1,
"V1 封锁格零登记(errors 仍仅清单外一条——%s)" % str(snap.get("errors", [])))
check((snap.get("cells", {}) as Dictionary).size() == mapped - 1,
"V1 封锁格剔除后作用集 = 派定数 −1(实测 %d)" % (snap.get("cells", {}) as Dictionary).size())
# ---- 相 4:封锁解除 ⇒ 作用集复位(进出圈复亮走回)----
_glow_blocked.erase(blocked_cell)
view.refresh()
snap = view.type_glow_probe()
check(_glow_key_set_equal(snap.get("cells", {}), expect),
"V1 封锁解除后作用集复位恒等(期望 %d,实测 %d)" % [expect.size(), (snap.get("cells", {}) as Dictionary).size()])
# ---- 相 5:开关 off/on(V7——off 态空+on 态逐位复现+棋盘态零扰动)----
var snap_before_off: Dictionary = view.type_glow_probe()
view.set_type_glow_enabled(false)
snap = view.type_glow_probe()
check(not bool(snap.get("enabled", true)), "V7 off 态开关读数=false")
check((snap.get("cells", {}) as Dictionary).is_empty(), "V7 off 态作用集为空(基线保护面)")
view.set_type_glow_enabled(true)
var snap_on: Dictionary = view.type_glow_probe()
check(bool(snap_on.get("enabled", false)), "V7 on 态开关读数=true(复现)")
check(_glow_map_equal(snap_on.get("cells", {}), snap_before_off.get("cells", {})),
"V7 off→on 作用集逐位复现(关前 %d 格/开后 %d 格,组号逐位一致)" % [
(snap_before_off.get("cells", {}) as Dictionary).size(), (snap_on.get("cells", {}) as Dictionary).size()])
check(_glow_key_set_equal(snap_on.get("cells", {}), expect),
"V7 off→on 作用集与独立重算期望集恒等(%d 格)" % (snap_on.get("cells", {}) as Dictionary).size())
var revealed_after: Array = ms.revealed_cells()
revealed_after.sort()
check(revealed_before == revealed_after and hero_before == ms.hero_pos() and flags_before == ms.flag_count(),
"V7 微光开关零玩法写入(已开集/英雄位/旗数三面对照逐位一致)")
# ---- 相 6:换图随 rebuild 重算(V3——旧板坐标零残留)----
_glow_type_overrides.clear()
_glow_blocked.clear()
var b2 = board_generator.generate(1, 0, _mk_stream("board_gen", 909), _db)
var ae2 = load("res://core/action_economy.gd").new(_db)
var ms2 = load("res://core/minesweeper_rules.gd").new(_db, ae2)
ms2.install_board(b2)
ae2.notify_round_start()
view.rebuild(b2, ms2)
var snap2: Dictionary = view.type_glow_probe()
var expect2: Dictionary = _glow_expected_set(b2, ms2)
check(_glow_key_set_equal(snap2.get("cells", {}), expect2),
"V3 换图 rebuild 后作用集按新板重算(期望 %d,实测 %d——零旧板残留)" % [expect2.size(), (snap2.get("cells", {}) as Dictionary).size()])
check((snap2.get("errors", []) as Array).is_empty(), "V3 换图清清单外登记(rebuild 清零)")
view.free()
# ---- 相 7:同板同序列 on/off 双实例结算逐位一致(契约 1 端到端面——不新起跑批)----
# 两个平行 core(同 db/同一 BoardData 产物/同初始展开),一侧挂生产默认开态 view、一侧挂
# 显式 off 态 view(差异唯一定位在类型微光开关——与渲染取证双帧同款隔离思想);同一动作
# 序列逐步对照摘要,on 侧若在微光派生中意外写板态,两侧即分叉。
var aeA = load("res://core/action_economy.gd").new(_db)
var msA = load("res://core/minesweeper_rules.gd").new(_db, aeA)
var aeB = load("res://core/action_economy.gd").new(_db)
var msB = load("res://core/minesweeper_rules.gd").new(_db, aeB)
msA.install_board(b)
msB.install_board(b)
aeA.notify_round_start()
aeB.notify_round_start()
var viewA = load("res://presentation/views/board_view.gd").new()
viewA.set_identity_resolver(_glow_fixture_identity)
viewA.rebuild(b, msA)
viewA.set_dig_glow_enabled(true)
var viewB = load("res://presentation/views/board_view.gd").new()
viewB.set_identity_resolver(_glow_fixture_identity)
viewB.rebuild(b, msB)
viewB.set_dig_glow_enabled(true)
viewB.set_type_glow_enabled(false) # off 态 = 基线保护面
var diverge := ""
var seq_steps := 0
while seq_steps < 12:
var act7: Dictionary = AutoPlayer.novice_action(msA)
if String(act7.get("kind", "stop")) == "stop":
break
var op7 := String(act7.get("kind", ""))
if op7 == "flag":
op7 = "flag_place"
var batch7 := [{"action": op7, "cell": act7.get("cell")}]
msA.settle_batch(batch7)
msB.settle_batch(batch7)
viewA.refresh()
viewB.refresh()
if _ms_digest(msA, aeA) != _ms_digest(msB, aeB):
diverge = "第 %d 步(%s)后摘要分叉" % [seq_steps, op7]
break
seq_steps += 1
check(diverge == "", "契约 1 同板同序列 on/off 两态结算逐位一致(%d 步对照——A 开态/B 关态)%s"
% [seq_steps, diverge])
check(seq_steps >= 5, "契约 1 对照前提:同序列 ≥5 步(实测 %d)" % seq_steps)
viewA.free()
viewB.free()
# 排雷 core 态摘要(同板同序列对照面——已开记录含 kind/digit、旗集、英雄位、AP、相位)
func _ms_digest(ms, ae) -> Array:
var b: Variant = ms.current_board()
var rev: Array = []
for c in ms.revealed_cells():
var rec: Dictionary = ms.reveal_record(c)
rev.append([int(c.x), int(c.y), String(rec.get("kind", "")), int(rec.get("digit", -1))])
rev.sort()
var flags: Array = []
for y in b.height:
for x in b.width:
var c := Vector2i(x, y)
if ms.has_flag(c):
flags.append([int(c.x), int(c.y)])
return [[int(ms.hero_pos().x), int(ms.hero_pos().y)], rev, flags, ae.ap, ae.phase, ms.flag_count()]
# ---- 地图战斗资源·身份面单元冒烟(格面资源批:roll 恰一次 / 雷安同池 / 幂等 / 清图批尾)----
func _smoke_reward_identity_unit() -> void:
+101 -1
View File
@@ -492,8 +492,108 @@ func _ready() -> void:
await get_tree().process_frame
if not _has_action(status_dig, "dig") or not ms.is_revealed(status_dig):
_problems.append("case⑯ 关闭后挖掘未到达(恢复无残留验证失守)")
# ---- case⑰ 未开格类型微光(迭代 30 卡① T2-③——活板面走查:作用集恒等/开格灭/移动复亮
# 与出圈灭/相位门控/开关两态零扰动。与冒烟段数据面断言分工:此处走真实点击路径) ----
if not bool(bv.type_glow_probe().get("enabled", false)):
_problems.append("case⑰ 微光默认态非开(生产默认开——Q4 丙案)")
if not (bv.type_glow_probe().get("errors", []) as Array).is_empty():
_problems.append("case⑰ 活板清单外登记非空(errors=%s——生产数据应全集合法)" % str(bv.type_glow_probe().get("errors", [])))
if bv.diggable_probe().is_empty():
var mv17a := _pick_frontier(bv, ms)
if mv17a.x == -9:
_problems.append("case⑰ 板面无 diggable 且无前沿可迫近(活板面前提异常)")
else:
_push_click(_pos(bv, mv17a), MOUSE_BUTTON_LEFT)
await get_tree().process_frame
await get_tree().process_frame
var digp17: Dictionary = bv.diggable_probe()
if digp17.is_empty():
_problems.append("case⑰ 板面仍无 diggable(活板面前提异常)")
else:
# ① 作用集恒等:cells 键集 == diggable 键集(生产身份回调面——未开格恒可查,F4)
var keys17: Array = (bv.type_glow_probe().get("cells", {}) as Dictionary).keys()
var eq17 := keys17.size() == digp17.size()
if eq17:
for k in keys17:
if not digp17.has(k):
eq17 = false
break
if not eq17:
_problems.append("case⑰ 作用集与可挖域不等(cells=%d diggable=%d)" % [keys17.size(), digp17.size()])
# ② 开格灭:真实点击挖开一格 → 该格退出作用集(S3 生命周期)
var dig17 := Vector2i(-9, -9)
for c in digp17:
if not ms.has_flag(c):
dig17 = c
break
if dig17.x == -9:
_problems.append("case⑰ 无无旗可挖格(板面异常)")
else:
if not (bv.type_glow_probe().get("cells", {}) as Dictionary).has(dig17):
_problems.append("case⑰ 待挖格不在作用集(%s)" % str(dig17))
_push_click(_pos(bv, dig17), MOUSE_BUTTON_LEFT)
await get_tree().process_frame
await get_tree().process_frame
if not ms.is_revealed(dig17):
_problems.append("case⑰ 真实点击未挖开目标格(%s)" % str(dig17))
if (bv.type_glow_probe().get("cells", {}) as Dictionary).has(dig17):
_problems.append("case⑰ 开格未灭(%s 仍在作用集——S3 生命周期破)" % str(dig17))
# ③ 移动复亮/出圈灭:点击前沿已开格移英雄 → 作用集随新 diggable 重算(恒等面承载两者)
var mv17 := _pick_frontier(bv, ms)
if mv17.x == -9:
_problems.append("case⑰ 无前沿可迫近(移动复亮断言取格中断)")
else:
_push_click(_pos(bv, mv17), MOUSE_BUTTON_LEFT)
await get_tree().process_frame
await get_tree().process_frame
var glowp2: Dictionary = bv.type_glow_probe()
var digp18: Dictionary = bv.diggable_probe()
var keys18: Array = (glowp2.get("cells", {}) as Dictionary).keys()
var eq18 := keys18.size() == digp18.size()
if eq18:
for k in keys18:
if not digp18.has(k):
eq18 = false
break
if not eq18:
_problems.append("case⑰ 移动后作用集与新可挖域不等(cells=%d diggable=%d——复亮/出圈灭破)" % [keys18.size(), digp18.size()])
# ④ 相位门控:结算相位(_dig_glow off)作用集空、微光开关读数不变;回挖掘相位逐位复位
var before_phase: Dictionary = (bv.type_glow_probe().get("cells", {}) as Dictionary).duplicate()
bv.set_dig_glow_enabled(false)
var glowp4: Dictionary = bv.type_glow_probe()
if not (glowp4.get("cells", {}) as Dictionary).is_empty():
_problems.append("case⑰ 结算相位未灭(作用集 %d 格——相位门控破)" % (glowp4.get("cells", {}) as Dictionary).size())
if not bool(glowp4.get("enabled", false)):
_problems.append("case⑰ 相位门控误改微光开关态(enabled 应保持 true)")
bv.set_dig_glow_enabled(true)
var glowp5: Dictionary = bv.type_glow_probe()
var after_phase: Dictionary = glowp5.get("cells", {})
var eq_phase := after_phase.size() == before_phase.size()
if eq_phase:
for k in before_phase:
if int(after_phase.get(k, -1)) != int(before_phase[k]):
eq_phase = false
break
if not eq_phase:
_problems.append("case⑰ 回挖掘相位作用集未逐位复位(%d→%d)" % [before_phase.size(), after_phase.size()])
# ⑤ 开关两态零扰动(case⑮ 同款升级到全板 revealed 摘要):off 前后 ms 逻辑态逐位一致
var b17 = ms.current_board()
var digest_before17: Array = []
for y in b17.height:
for x in b17.width:
digest_before17.append(ms.is_revealed(Vector2i(x, y)))
bv.set_type_glow_enabled(false)
var digest_off17: Array = []
for y in b17.height:
for x in b17.width:
digest_off17.append(ms.is_revealed(Vector2i(x, y)))
if digest_off17 != digest_before17:
_problems.append("case⑰ 微光开关改写逻辑态(两态结算逐位一致 🔴 契约破)")
bv.set_type_glow_enabled(true)
if not bool(bv.type_glow_probe().get("enabled", false)):
_problems.append("case⑰ 微光开关未恢复开态")
if _problems.is_empty():
print("[ui_input_probe] PASS — 悬停金框/禁入信号/路径预览/flag_place/chord/dig/英雄落位/影子构成/朝向/缩格重排/缺位回退/变异角标/视野迷雾/状态面板输入锁 十七断言全绿(真实 push_input 路径——⑫图上命中⑬画布拟合 2026-09-28 增,⑭变异角标 2026-09-29 增,④b 旗纹理槽回归 2026-10-02 增,⑮视野迷雾 2026-10-03 增〔+提灯接线断言 2026-10-05 迭代14⑤〕,⑯状态面板输入锁 2026-10-06 迭代17卡③增)")
print("[ui_input_probe] PASS — 悬停金框/禁入信号/路径预览/flag_place/chord/dig/英雄落位/影子构成/朝向/缩格重排/缺位回退/变异角标/视野迷雾/状态面板输入锁/未开格类型微光 十八断言全绿(真实 push_input 路径——⑫图上命中⑬画布拟合 2026-09-28 增,⑭变异角标 2026-09-29 增,④b 旗纹理槽回归 2026-10-02 增,⑮视野迷雾 2026-10-03 增〔+提灯接线断言 2026-10-05 迭代14⑤〕,⑯状态面板输入锁 2026-10-06 迭代17卡③增,⑰未开格类型微光 2026-10-17 迭代30卡①增)")
get_tree().quit(0)
else:
for p in _problems:
+1
View File
@@ -77,6 +77,7 @@ func _suites() -> Array:
{"path": "res://tests/unit/core/config_db_cross_validator_test.gd", "expect": 0}, # 跨表校验(正向零错/悬空 skill_id/side 反向)
{"path": "res://tests/unit/presentation/hud_presenter_test.gd", "expect": 0}, # HUD 投影文案(逐拒绝原因/兜底不空)
{"path": "res://tests/unit/presentation/battle_narrator_test.gd", "expect": 0}, # 战斗叙述(数值带入/胜负互异/收束态恒定)
{"path": "res://tests/unit/presentation/type_glow_test.gd", "expect": 0}, # 迭代 30 卡① T2-②——未开格类型微光数据面(九型归组封闭/清单外显式失败/冻结色相只准动明度/α170·带宽cell÷8锚定/合成空间阶梯单调+模型CR≥1.20/probe API 形状)
]
@@ -0,0 +1,173 @@
# tests/unit/presentation/type_glow_test.gd — 未开格类型微光数据面直验(迭代 30 卡① T2-②)
# 断言面 = 纯函数/常量域(无渲染):① 九 cell_type → 五组封闭判定(含清单外显式失败、
# 禁子串匹配、与卡顶带 tg_band 的归组差异锚定——Q3 甲案两带并存);② 五组色值冻结面
# (HSV 对照 art-bible §4.2 冻结值只准动明度档——2026-10-17 群伤组一案停机呈报后用户裁
# #A61D37,本测试即该裁定的守门件);③ 呈现参数锚定(α170/带宽 cell÷8);④ 合成空间
# 阶梯单调 + 相邻组模型 CR ≥1.20(色弱灰度互辨的数据面预判,终审在渲染取证探针实测);
# ⑤ probe API 形状(默认开/空态键集)。
# 运行:godot --headless --path game --script res://tests/unit/presentation/type_glow_test.gd
extends SceneTree
const Kit := preload("res://tests/unit/test_kit.gd")
# --script 模式无全局标识符(class_name 不可用)——使用方 preload
const UiKitScript := preload("res://presentation/views/ui_kit.gd")
const BoardViewScript := preload("res://presentation/views/board_view.gd")
# art-bible §4.2 样张 v2 冻结五组(F3 抄录表——本测试的冻结面基准)
const FROZEN := {
"BUFF": Color8(0xD6, 0xC1, 0x3A), "HEAL": Color8(0x5C, 0xB8, 0x3A),
"PIERCE": Color8(0x2E, 0x9E, 0x9E), "MAGIC": Color8(0x7B, 0x5B, 0xD6),
"AOE": Color8(0xFF, 0x2D, 0x55),
}
# 九 cell_type 全值域 → 期望组号(art-bible §4.2 成员集合逐字照抄)
const TYPE_EXPECT := {
"attack": UiKitScript.GLOW_BUFF, "defense": UiKitScript.GLOW_BUFF,
"weaken": UiKitScript.GLOW_BUFF, "action": UiKitScript.GLOW_BUFF,
"gp": UiKitScript.GLOW_BUFF, "heal": UiKitScript.GLOW_HEAL,
"pierce": UiKitScript.GLOW_PIERCE, "blast": UiKitScript.GLOW_AOE,
"magic": UiKitScript.GLOW_MAGIC,
}
# provisional 明度档终值(2026-10-17 用户裁后)——逐位锚定,谁改色值这里红
const IMPL := {
"BUFF": Color8(247, 223, 67), "HEAL": Color8(104, 208, 66),
"PIERCE": Color8(48, 166, 166), "MAGIC": Color8(123, 91, 214),
"AOE": Color8(166, 29, 55),
}
# 实现常量直引(禁 class get() 动态取——parse 层就不许)
const IMPL_CONSTS := {
"BUFF": UiKitScript.TG_GLOW_BUFF, "HEAL": UiKitScript.TG_GLOW_HEAL,
"PIERCE": UiKitScript.TG_GLOW_PIERCE, "MAGIC": UiKitScript.TG_GLOW_MAGIC,
"AOE": UiKitScript.TG_GLOW_AOE,
}
const CONST_NAME := {
"BUFF": "TG_GLOW_BUFF", "HEAL": "TG_GLOW_HEAL", "PIERCE": "TG_GLOW_PIERCE",
"MAGIC": "TG_GLOW_MAGIC", "AOE": "TG_GLOW_AOE",
}
func _init() -> void:
_test_grouping_closed()
_test_frozen_hue_face()
_test_render_params()
_test_gamma_ladder()
_test_probe_api_shape()
quit(Kit.finish("type_glow_test"))
# ① 归组封闭:九值全域映射+清单外显式失败+禁子串+两带差异锚定
func _test_grouping_closed() -> void:
for t in TYPE_EXPECT:
var got: int = UiKitScript.glow_group_for_cell_type(t)
Kit.check("九型封闭:%s → 组%d(期望组%d)" % [t, got, int(TYPE_EXPECT[t])], got == int(TYPE_EXPECT[t]))
# 清单外值 = GLOW_NONE(调用方显式登记,禁静默空光;大宝箱未来第十型落数据时必须同步加归组,
# 否则本通道报红——这是该纪律的机检防线)
for bad in ["keystone", "", "ATTACK", "aoe", "blast_mine", " heal"]:
Kit.check("清单外值「%s」→ GLOW_NONE(禁静默空光)" % bad,
UiKitScript.glow_group_for_cell_type(bad) == UiKitScript.GLOW_NONE)
# 与卡顶带 tg_band 的归组差异锚定(Q3 甲案两带并存、禁互引):blast 在格面带归群伤、
# 在卡顶带归魔法(E5 回滚既存行为)——若将来误把两带统一,本断言红
var band_blast: Color = UiKitScript.tg_band("blast_charge")
Kit.check("两带并存锚定:卡顶带 blast→TG_MAGIC(E5 回滚既存行为未动)", band_blast == UiKitScript.TG_MAGIC)
Kit.check("两带并存锚定:格面微光 blast→群伤组(与卡顶带刻意不同)",
UiKitScript.glow_group_for_cell_type("blast") == UiKitScript.GLOW_AOE)
# 色值出口:五组各取本组常量;无组/越界组号 → 全透明(不画)
Kit.check("glow_color_for_group 群伤组 = TG_GLOW_AOE",
UiKitScript.glow_color_for_group(UiKitScript.GLOW_AOE) == UiKitScript.TG_GLOW_AOE)
Kit.check("glow_color_for_group 无组(GLOW_NONE)→ 全透明",
UiKitScript.glow_color_for_group(UiKitScript.GLOW_NONE) == Color(0, 0, 0, 0))
Kit.check("glow_color_for_group 越界组号 → 全透明",
UiKitScript.glow_color_for_group(99) == Color(0, 0, 0, 0))
# ② 冻结色相面:逐组 HSV 对照 art-bible §4.2 冻结值——只准动明度(ΔH≤0.2°/ΔS≤0.01)
func _test_frozen_hue_face() -> void:
for g in FROZEN:
var f: Color = FROZEN[g]
var m: Color = IMPL[g]
var impl_const: Color = IMPL_CONSTS[g]
Kit.check("实现值锚定:%s = #%02X%02X%02X(色值单点 ui_kit,逐位钉死)" % [CONST_NAME[g],
int(m.r8), int(m.g8), int(m.b8)], impl_const == m)
var dh: float = absf(m.h - f.h)
dh = minf(dh, 1.0 - dh) # 色相环回绕
var ds: float = absf(m.s - f.s)
Kit.check("冻结色相保持:%s ΔH=%.3f° ≤0.2° 且 ΔS=%.4f ≤0.01(只动明度档)" % [g, dh * 360.0, ds],
dh <= 0.2 / 360.0 and ds <= 0.01)
# 群伤组专锚(2026-10-17 停机呈报→用户裁 #A61D37):H/S 与冻结逐位一致、V=0.65 档
var aoe_f: Color = FROZEN["AOE"]
var aoe_m: Color = UiKitScript.TG_GLOW_AOE
Kit.check("群伤组专项:H 与冻结 #FF2D55 差 %.4f°(零移动)" % (absf(aoe_m.h - aoe_f.h) * 360.0),
absf(aoe_m.h - aoe_f.h) < 0.001)
Kit.check("群伤组专项:S 与冻结差 %.4f ≤0.01(8bit 量化噪声级)" % absf(aoe_m.s - aoe_f.s),
absf(aoe_m.s - aoe_f.s) <= 0.01)
Kit.check("群伤组专项:V=0.65 明度档( provisional 域内)", absf(aoe_m.v - 166.0 / 255.0) < 0.002)
# ③ 呈现参数锚定(art-bible §4.2 v2 冻结参数)
func _test_render_params() -> void:
Kit.check("泛光强度 α = 170/255(v2 呈现参数冻结)",
is_equal_approx(UiKitScript.TYPE_GLOW_ALPHA, 170.0 / 255.0))
Kit.check("带宽 = cell÷8(TYPE_GLOW_BAND_FRAC=0.125)",
is_equal_approx(UiKitScript.TYPE_GLOW_BAND_FRAC, 0.125))
Kit.check("组号常量域连续(GLOW_NONE=0…GLOW_MAGIC=5)",
UiKitScript.GLOW_NONE == 0 and UiKitScript.GLOW_BUFF == 1 and UiKitScript.GLOW_HEAL == 2
and UiKitScript.GLOW_PIERCE == 3 and UiKitScript.GLOW_AOE == 4 and UiKitScript.GLOW_MAGIC == 5)
# ④ 合成空间阶梯:模型域单调 + 相邻组 CR ≥1.20(色弱互辨数据面预判;终审=探针实测)
func _test_gamma_ladder() -> void:
# 合成模型(ui_kit 头注登记口径):带内均值 ≈ 0.49×色值 + 0.51×瓦片(BRICK 未开掩尘地砖)
var k := 0.49
var tile: Color = UiKitScript.BRICK
var comp := func(c: Color) -> Color:
return Color(k * c.r + (1.0 - k) * tile.r, k * c.g + (1.0 - k) * tile.g, k * c.b + (1.0 - k) * tile.b)
var order := ["BUFF", "HEAL", "PIERCE", "MAGIC", "AOE"]
var prev_lum := 2.0
for g in order:
var cc: Color = comp.call(IMPL[g])
var lum: float = _rel_lum709(cc)
Kit.check("阶梯单调:%s 合成亮度 %.4f < 上一档 %.4f(伽马合成空间)" % [g, lum, prev_lum], lum < prev_lum)
prev_lum = lum
for i in range(order.size() - 1):
var a: Color = comp.call(IMPL[order[i]])
var b: Color = comp.call(IMPL[order[i + 1]])
var cr: float = _wcag_cr(a, b)
Kit.check("相邻组模型 CR:%s/%s = %.3f ≥1.20(色弱互辨预判)" % [order[i], order[i + 1], cr], cr >= 1.20)
# 首版排反的那一对专项:群伤线性亮度必须低于魔法(线性域序 = WCAG 判据域)
var aoe_lin: float = _rel_lum_lin(comp.call(IMPL["AOE"]))
var magic_lin: float = _rel_lum_lin(comp.call(IMPL["MAGIC"]))
Kit.check("群伤/魔法线性亮度序:群伤 %.4f < 魔法 %.4f(首版 CIE L* 排反事故的守门)" % [aoe_lin, magic_lin],
aoe_lin < magic_lin)
# ⑤ probe API 形状:默认开+空态键集(无板直取——不依赖棋盘装配)
func _test_probe_api_shape() -> void:
var view = BoardViewScript.new()
var snap: Dictionary = view.type_glow_probe()
Kit.check("probe 键集 = {enabled, cells, errors}",
snap.has("enabled") and snap.has("cells") and snap.has("errors") and snap.size() == 3)
Kit.check("probe 默认态:enabled=true(Q4 丙案生产默认开)", bool(snap.get("enabled", false)))
Kit.check("probe 默认态:作用集空(无板未派生)", (snap.get("cells", {}) as Dictionary).is_empty())
Kit.check("probe 默认态:清单外登记空", (snap.get("errors", []) as Array).is_empty())
view.set_type_glow_enabled(false)
Kit.check("set_type_glow_enabled(false) → probe enabled=false(无板不崩)",
not bool(view.type_glow_probe().get("enabled", true)))
view.free()
# Rec.709 相对亮度(sRGB 值域——灰度互辨通道读数,与 shot_checker 同口径)
static func _rel_lum709(c: Color) -> float:
return 0.2126 * c.r + 0.7152 * c.g + 0.0722 * c.b
# WCAG 线性域相对亮度(反伽马)
static func _rel_lum_lin(c: Color) -> float:
var lin := func(ch: float) -> float:
return ch / 12.92 if ch <= 0.04045 else pow((ch + 0.055) / 1.055, 2.4)
return 0.2126 * lin.call(c.r) + 0.7152 * lin.call(c.g) + 0.0722 * lin.call(c.b)
# WCAG 对比对(线性域 +0.05 偏移)
static func _wcag_cr(a: Color, b: Color) -> float:
var la: float = _rel_lum_lin(a)
var lb: float = _rel_lum_lin(b)
return (maxf(la, lb) + 0.05) / (minf(la, lb) + 0.05)
@@ -0,0 +1 @@
uid://bykt02pgt6mbf