feat(迭代38卡④ 明细收敛批): 扩展行型收敛——四个专长实名行型(fx_reveal/mine_neutralized/insurance_used/fx_preview_area)收敛为单一 extended 行型+effect_id 载荷(六发射点:orchestrator 三处+grant_segment 三处,display_name/value/highlight 逐字不动);a2 _emit_row 扩展行必载 effect_id 校验(emit 之前,违约 assert 炸零发射);a3 fx_preview_area 来源类别空串违约修复(→fx_warning_beacon,对齐 fx_seal_smith 先例);a4 头注换代两层词表(基础 9 类+扩展行 1 类+四效果身份发射点指引);a5 呈现零改动核验(_format_row fallback 承载、AGG_TYPES 不含扩展行);a6 裁点甲重锚(parity_anchor freeze 改后行为立新锚+复闸三变体双面全同;改前对拍绿基线在案——stash 干净跑批实测);验收=wr2 断言五条(insurance_used〔V6 扩展〕+fx_reveal 兑付/落空〔真实揭示管线〕+mine_neutralized/fx_preview_area〔treasure 套件首次 feats 注入〕)+wr2-np 负面小套件(extended_row_violation_test,expect=1 negative,manifest 登记行)+wr3 并⑤断言;撤销证明三条实测红绿复现;JSONL 读数对拍 PASS(type 迁移计数与 T0 基线对账 264/304/0/0,字段口径外零差异——type/effect_id/source_class/source 四字段,source=logger:133 同值改名副本);改动面封闭(基础 9 类零触碰、豁免集合零变化、数值随机零变化)

This commit is contained in:
DKChan committed 2026-10-26 08:52:14 +08:00
1 parent ce49a8eec2
commit d5a03db1a8
7 files changed
+174 -12

No files matched your search

+17 -5
View File
@@ -12,11 +12,18 @@ signal round_started(round_index: int)
signal board_changed()
signal settlement_snapshot(snap: Dictionary) ## 每结算末尾的全量状态快照(playtest_logger 消费——JSONL 数值分析)
# 明细流水行型协议(战斗结算 Core Rule;9 类;
# 明细流水行型协议(战斗结算 Core Rule;两层词表——基础 9 类封闭+扩展行 1 类;
# 清单改动须同步 battle_log_view 的 _format_row 与本文档头注):
# 效果行 effect · 伤害行 damage · 治疗行 heal ·
# 基础 9 类:效果行 effect · 伤害行 damage · 治疗行 heal ·
# 奖励未生效行 reward_ineffective · 封印未中行 seal_miss · 施放行 cast · 衰减行 decay ·
# 判定行 verdict · 高光行 highlight
# 扩展行:行型值=extended(单一扩展行型),效果身份由 effect_id 载荷携带(_emit_row 强制必载,
# 缺载即断言失败零发射)。现役效果身份与发射点:
# fx_reveal(专长揭示掷格——battle_orchestrator _flush_pending_reveals 三发射点:兑付行+落空行×2)
# mine_neutralized(封印匠·危险格中立移除——grant_segment 封印命中 remove_mines 面)
# fx_preview_area(示警烟火·周围资源类型——grant_segment 封印命中 preview_area 面)
# insurance_used(完美保险·标错豁免——grant_segment 标错 forgive_seal_miss 面)
# 新增专长效果只新增 effect_id 身份不再新增行型;行型词表改动须同步 owner 文档「扩展行」节。
# 行来源断言豁免集合(行型层)——判定段 / 衰减段本就无棋盘来源,勿一刀切误炸
const SOURCE_EXEMPT_TYPES := ["decay", "verdict"]
# 行来源断言豁免集合(来源类别层)——事实来源是单位 / 清图而非棋盘格;
@@ -146,7 +153,7 @@ func _flush_pending_reveals(ri: int) -> void:
if not minesweeper.is_revealed(c) and not board.is_mine(c):
cands.append(c)
if cands.is_empty():
_emit_row("fx_reveal", "fx_reveal", Vector2i(-1, -1), ri, "揭示落空(无未揭示安全格)", 0, {}, "")
_emit_row("extended", "fx_reveal", Vector2i(-1, -1), ri, "揭示落空(无未揭示安全格)", 0, {"effect_id": "fx_reveal"}, "")
continue
var rg: RandomNumberGenerator = rng.open_stream("fx_reveal")
var cell: Vector2i = cands[rg.randi_range(0, cands.size() - 1)]
@@ -154,12 +161,12 @@ func _flush_pending_reveals(ri: int) -> void:
var ok: bool = minesweeper.fx_reveal(cell)
_fx_revealing = false
if not ok:
_emit_row("fx_reveal", "fx_reveal", Vector2i(-1, -1), ri, "揭示落空(格子不可揭)", 0, {}, "")
_emit_row("extended", "fx_reveal", Vector2i(-1, -1), ri, "揭示落空(格子不可揭)", 0, {"effect_id": "fx_reveal"}, "")
continue
var snap = _reward_economy.query_full_snapshot(cell)
if snap is Dictionary:
_grant_routed(snap, "positive", "fx_reveal", cell, ri, "fx_reveal")
_emit_row("fx_reveal", "fx_reveal", cell, ri, "专长揭示", 0, {}, "fx_reveal")
_emit_row("extended", "fx_reveal", cell, ri, "专长揭示", 0, {"effect_id": "fx_reveal"}, "fx_reveal")
# 补充事实面(fact 取值第二优先源——payload 未携带的运行态读数)
@@ -1260,6 +1267,11 @@ func _reset_round_facts() -> void:
# ---- 行协议(Core Rule:公共字段 + 行类型特有字段)----
func _emit_row(type: String, source_class: String, origin_cell: Vector2i, ri: int, display: String, value: int, extra: Dictionary, highlight: String = "") -> void:
# 扩展行必载效果身份(行协议 fail-fast——违约行先炸后不发,零发射;开发期违约即炸,
# 与 _assert_row_source 同族 assert 形态)
if type == "extended" and String(extra.get("effect_id", "")) == "":
assert(false, "[战斗结算] 扩展行缺 effect_id 载荷——行协议违约")
return
_assert_row_source(type, source_class, origin_cell)
var row := {
"type": type,
+4 -4
View File
@@ -268,7 +268,7 @@ func _judge_seal(evt: Dictionary, ri: int) -> void:
"remove_mines":
var removed: Array[Variant] = oc.minesweeper.remove_mines_in([cell])
if not removed.is_empty():
oc._emit_row("mine_neutralized", "fx_seal_smith", cell, ri, "封印匠·危险格中立移除", 0, {}, "")
oc._emit_row("extended", "fx_seal_smith", cell, ri, "封印匠·危险格中立移除", 0, {"effect_id": "mine_neutralized"}, "")
"preview_area":
var board_p: BoardData = oc.minesweeper.current_board()
var types: Array[Variant] = []
@@ -278,7 +278,7 @@ func _judge_seal(evt: Dictionary, ri: int) -> void:
types.append(String((n_snap as Dictionary).get("cell_type", "")))
if oc.treasure_inventory != null and not types.is_empty():
oc.treasure_inventory.apply_overlay("reward_preview", board_p.neighbors8(cell))
oc._emit_row("fx_preview_area", "", cell, ri, "示警烟火·周围资源类型", 0, {"types": types}, "")
oc._emit_row("extended", "fx_warning_beacon", cell, ri, "示警烟火·周围资源类型", 0, {"effect_id": "fx_preview_area", "types": types}, "")
# 雷区图谱(封印流——标中雷额外获得一份格面资源:封印开始产出对标开格;
# 复用 grant 单点,额外兑付走同一 _grant_routed 正向路由)
for h in fh_hits:
@@ -306,8 +306,8 @@ func _judge_seal(evt: Dictionary, ri: int) -> void:
# _perfect_forgiven,绝不回写 _perfect_hit_false=契约 2 计数纯净;发射时机与消费点零改动=契约 3)
oc._perfect_forgiven += 1
_apply_counter_hit(h) # 豁免同时记账(insurance_used battle 域——下次标错 when lt 1 不再命中)
oc._emit_row("insurance_used", "seal_miss", cell, ri, "完美保险·标错豁免", 0,
{"forgiven": oc._perfect_forgiven}, "")
oc._emit_row("extended", "seal_miss", cell, ri, "完美保险·标错豁免", 0,
{"effect_id": "insurance_used", "forgiven": oc._perfect_forgiven}, "")
var hl := "perfect_broken" if oc._perfect_hit_false - oc._perfect_forgiven == 1 else ""
oc._emit_row("seal_miss", "seal_miss", cell, ri, "封印未中", 0, {}, hl)
var snap = oc._reward_economy.grant(cell)
+3 -3
View File
@@ -49,7 +49,7 @@
"rows": 9216
},
"perfect": {
"sha256": "a37ba2ca47f8619f22aa3dd8a761fffc71916d6c3172f5ff0aeb2b0557aba7e5",
"sha256": "962b6f68440bcf95f2b21ac4cc07a8523655f044ae6355aa674d0db27bdecc1d",
"rows": 60774
}
}
@@ -103,7 +103,7 @@
"rows": 9670
},
"perfect": {
"sha256": "f6bb9fd5ba5fc183fd29adbb00ae526a5c6852e7f52f9f597a0b4d853baf05d6",
"sha256": "06cb4dcc26c56343c61bc8d972238be81b88e3132d7e9b274399f4cfd04cb56e",
"rows": 62765
}
}
@@ -157,7 +157,7 @@
"rows": 7124
},
"perfect": {
"sha256": "7d16b4efb2bb48355a7919011b265de83ff907bd9c83818dc66e69108967d5a3",
"sha256": "c3d0d37c5f40d208fb35d323b4c7a11c4f7ebe77f9f0eaba0fc3445f00971277",
"rows": 59435
}
}
+1
View File
@@ -70,6 +70,7 @@ func _suites() -> Array:
{"path": "res://tests/unit/battle_settlement/round_flow_skill_pool_test.gd", "expect": 0}, # 轮内流与技能池宿主(全战斗回放/轮内刷新与 V5 快照单源/遭遇区间骰/结算五段全序/技能池数据面·回放独立·EV 锚·死亡打断·校验拒绝/妨害附加/四桶计数——批 B 自 smoke_all 迁入)
{"path": "res://tests/unit/battle_settlement/variant_placement_test.gd", "expect": 0}, # 变异布置宿主(战内布置段门控与数量公式/全局变异兜底回落与资源面偏斜——批 B 自 smoke_all 迁入)
{"path": "res://tests/unit/battle_settlement/skill_unlock_test.gd", "expect": 0}, # Boss 半血解锁技·容器机制(门谓词/闩不可逆/双面过滤/保底不逼锁定/零扰动)
{"path": "res://tests/unit/battle_settlement/extended_row_violation_test.gd", "expect": 1, "negative": true}, # 卡④ wr2-np 扩展行必载 effect_id 负路径(行协议运行期违约面首个负面套件——独立进程豁免错误扫描)
{"path": "res://tests/unit/contracts/system_contracts_test.gd", "expect": 0},
{"path": "res://tests/unit/contracts/source_guard_test.gd", "expect": 0}, # 源码契约宿主(project.godot 警告配置与类型闸/产品码卫生死代码·影子账·层序单口——批 B 自 smoke_all 迁入;被测对象=仓库自身源文件,裁决五新设)
{"path": "res://tests/unit/presentation/ui_font_asset_test.gd", "expect": 0},
@@ -0,0 +1,38 @@
# 致命类用例(独立进程,期望退出码 1)——迭代 38 卡④ wr2-np:扩展行必载 effect_id 负路径断言
# 违约直调 _emit_row(type=extended 无 effect_id 载荷)→ a2 行协议校验 assert 炸(debug 实况,
# 输出中的 SCRIPT ERROR 为其测试目标——负面套件全文折叠)→ 零发射;
# 回归态(校验段被移除)= 违约行照发 → 退出码 2 ≠ 期望 1(闸红)。
# 独立进程理由:GDScript assert 失败必产 SCRIPT ERROR 且不可捕获,非负面套件会被 run_all 错误
# 扫描判红(迭代 38 卡④ grilling 第二轮同款结论——负面豁免机制为此存在,counter_prefix 先例)。
# 运行:godot --headless --path game --script res://tests/unit/battle_settlement/extended_row_violation_test.gd
extends SceneTree
const Kit := preload("res://tests/unit/test_kit.gd")
func _init() -> void:
var db = load("res://core/config_db.gd").new()
if not db.load_tables("res://data/tables", false):
push_error("[extended_row_violation_test] 表加载失败")
quit(2)
return
var rng = load("res://core/rng_service.gd").new()
rng.set_master_seed(74100)
var oc = load("res://core/battle_orchestrator.gd").new(db, rng)
var hits: Array = []
oc.step_row.connect(func(row): hits.append(row))
# 违约直调(缺 effect_id 载荷——行协议违约):debug 下 a2 校验 assert 炸中断 _emit_row 本体,
# 控制流回到本函数(headless assert 语义=中断当前函数不中止运行);release 下 return 零发射同效
oc._emit_row("extended", "fx_reveal", Vector2i.ZERO, 1, "负路径", 0, {}, "")
if not hits.is_empty():
push_error("[extended_row_violation_test] 违约行被发射——a2 校验失守(回归)")
quit(2)
return
# 对照组:合法扩展行(带 effect_id)照常发射——校验只拦违约形态
oc._emit_row("extended", "fx_reveal", Vector2i.ZERO, 1, "合法对照", 0, {"effect_id": "fx_reveal"}, "")
if hits.size() != 1 or String(hits[0].get("type", "")) != "extended":
push_error("[extended_row_violation_test] 合法扩展行未发射——校验误拦(回归)")
quit(2)
return
print("[extended_row_violation_test] 违约零发射+合法照发(a2 校验在位)")
quit(1)
@@ -38,6 +38,7 @@ func _init() -> void:
_smoke_settlement_segments()
_smoke_skill_pool()
_smoke_counter_buckets()
_smoke_extended_rows()
quit(Kit.finish("round_flow_skill_pool_test"))
@@ -994,6 +995,8 @@ func _smoke_counter_buckets() -> void:
"node_type": "battle", "ledger": load("res://core/run_ledger.gd").new()}
var oA = RigS.new(_db, _fresh_rng(779401))
oA.begin_battle(encA)
var v6_rows: Array = []
oA.step_row.connect(func(row): v6_rows.append(row)) # 卡④ wr2①——标错行捕获(真实 step_row 发射面)
var safe_a: Variant = null
for ca in oA.minesweeper.current_board().all_cells():
if not oA.minesweeper.current_board().is_mine(ca):
@@ -1001,8 +1004,62 @@ func _smoke_counter_buckets() -> void:
break
oA._grant_seg._judge_seal({"cell": safe_a, "hit": false}, 1)
Kit.check("V6 第1战标错落 battle 桶 insurance_used=1(_apply_counter_hit 静态写口经 ctx)〔无状态化 V6〕", EffectEngine.counter(oA.counters_ctx, "insurance_used") == 1)
# 卡④ wr2①——完美保险标错行=单一扩展行型(原 insurance_used 实名行型收敛)
var v6_ins := {}
for r in v6_rows:
if String(r.get("effect_id", "")) == "insurance_used":
v6_ins = r
Kit.check("卡④ wr2①:完美保险标错行 type=extended 且 effect_id=insurance_used(effect_id 必载)〔明细收敛 V2〕",
not v6_ins.is_empty() and String(v6_ins.get("type", "")) == "extended"
and String(v6_ins.get("source_class", "")) == "seal_miss" and bool(v6_ins.get("forgiven", 0)))
var encB = encA.duplicate(true)
encB["battle_index"] = 2
var oB = RigS.new(_db, _fresh_rng(779402))
oB.begin_battle(encB)
Kit.check("V6 battle 桶随宿主销毁:次战新编排器 fx_counters_battle 天然空,读 insurance_used=0〔无状态化 V6〕", EffectEngine.counter(oB.counters_ctx, "insurance_used") == 0)
# ---- 扩展行收敛(迭代 38 卡④)——fx_reveal 兑付行与落空行经真实揭示管线发射 ----
# 驱动形态:装配揭示类专长(geologist)的编排器上走生产同一入队/兑付管线
# (_fx_enqueue_reveal → _flush_pending_reveals——grant/damage 揭示订阅面的共用出口)。
# V6 形态先例同源:直入真实管线缝(_judge_seal 直调同款口径)。
func _smoke_extended_rows() -> void:
print("--- 扩展行收敛:fx_reveal 兑付/落空 ---")
var RigS = load("res://core/battle_orchestrator.gd") # 局部装配别名(_smoke_counter_buckets 同款——跨函数不共享局部量)
var enc_fx = {"level_index": 1, "node_depth": 0, "hero_hp": 40, "hero_atk": 8, "hero_max_hp": 40,
"hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "attune_charges": {},
"feats": ["geologist"], "tree_rules": [], "battle_index": 1,
"bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "voucher_rap": 0, "voucher_time_sand": 0,
"node_type": "battle", "ledger": load("res://core/run_ledger.gd").new()}
# ② 兑付行:有未揭示安全格 → 掷格揭示成功 → 扩展行带真实格与 fx_reveal 身份
var o_fx = RigS.new(_db, _fresh_rng(779501))
o_fx.begin_battle(enc_fx)
var fx_rows: Array = []
o_fx.step_row.connect(func(row): fx_rows.append(row))
o_fx._fx_enqueue_reveal()
o_fx._flush_pending_reveals(1)
var paid := {}
for r in fx_rows:
if String(r.get("effect_id", "")) == "fx_reveal":
paid = r
Kit.check("卡④ wr2②:专长揭示兑付行 type=extended 且 effect_id=fx_reveal 且来源格真实〔明细收敛 V2〕",
not paid.is_empty() and String(paid.get("type", "")) == "extended"
and Vector2i(paid["origin_cell"][0], paid["origin_cell"][1]).x >= 0)
# ③ 落空行:无未揭示安全格态(全安全格先揭)→ 候选空 → 落空归因行(origin_cell 越界哨值)
var o_void = RigS.new(_db, _fresh_rng(779502))
o_void.begin_battle(enc_fx)
var void_rows: Array = []
o_void.step_row.connect(func(row): void_rows.append(row))
for cv in o_void.minesweeper.current_board().all_cells():
if not o_void.minesweeper.current_board().is_mine(cv):
o_void.minesweeper.fx_reveal(cv)
o_void._fx_enqueue_reveal()
o_void._flush_pending_reveals(1)
var missed := {}
for r in void_rows:
if String(r.get("effect_id", "")) == "fx_reveal" and String(r.get("display_name", "")).contains("落空"):
missed = r
Kit.check("卡④ wr2③:揭示落空行 type=extended 且 effect_id=fx_reveal 且 origin_cell=越界哨值〔明细收敛 V2〕",
not missed.is_empty() and String(missed.get("type", "")) == "extended"
and Vector2i(missed["origin_cell"][0], missed["origin_cell"][1]).x == -1)
@@ -300,6 +300,60 @@ func _init() -> void:
and (adr14.runner()._attunement as Array) == snap14,
"装配不可变:战斗运行态装配集与冒险装配集逐位同源(运行零写口——快照对比)")
# ⑮ 扩展行收敛(迭代 38 卡④ wr2④⑤——套件首次 feats 注入,V6 形态自 round_flow_skill_pool_test 移植:
# enc 带 feats 键+ledger,专长触发经 _judge_seal 真实封印判定管线)
var enc_feats = {"level_index": 1, "node_depth": 0, "hero_hp": 40, "hero_atk": 8, "hero_max_hp": 40,
"hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "attune_charges": {},
"feats": ["seal_smith"], "tree_rules": [], "battle_index": 1,
"bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "voucher_rap": 0, "voucher_time_sand": 0,
"node_type": "battle", "ledger": load("res://core/run_ledger.gd").new()}
var rng15 = load("res://core/rng_service.gd").new(); rng15.set_master_seed(341)
var ob15 = orch_s.new(db, rng15)
ob15.begin_battle(enc_feats)
var rows15: Array = []
ob15.step_row.connect(func(row): rows15.append(row))
# ④ 封印匠(每第 3 次封印命中→危险格中立移除):三枚不同雷格各封一次
#(_judge_seal 每格去重——_seal_consumed 以危险格实例为键,同格重复命中不计数)
var mines15: Array = []
for cv in ob15.minesweeper.current_board().all_cells():
if ob15.minesweeper.current_board().is_mine(cv):
mines15.append(cv)
if mines15.size() >= 3:
break
Kit.check_quiet(mines15.size() == 3, "封印匠:板上存在三枚危险格(断言前提)")
if mines15.size() == 3:
for mv in mines15:
ob15._grant_seg._judge_seal({"cell": mv, "hit": true}, 1)
var smith_row := {}
for r in rows15:
if String(r.get("effect_id", "")) == "mine_neutralized":
smith_row = r
Kit.check_quiet(not smith_row.is_empty() and String(smith_row.get("type", "")) == "extended"
and String(smith_row.get("source_class", "")) == "fx_seal_smith",
"封印匠第三次命中:危险格中立移除行=扩展行型(type=extended+effect_id=mine_neutralized)〔明细收敛 V2〕")
# ⑤ 示警烟火(封印命中→周围资源类型预告):fx_preview_area 扩展行+来源类别修复断言(wr3)
var enc_beacon = enc_feats.duplicate(true)
enc_beacon["feats"] = ["warning_beacon"]
var rng15b = load("res://core/rng_service.gd").new(); rng15b.set_master_seed(342)
var ob15b = orch_s.new(db, rng15b)
ob15b.begin_battle(enc_beacon)
var rows15b: Array = []
ob15b.step_row.connect(func(row): rows15b.append(row))
var cell15b: Variant = null
for cv in ob15b.minesweeper.current_board().all_cells():
if not ob15b.minesweeper.current_board().is_mine(cv):
cell15b = cv
break
if cell15b != null:
ob15b._grant_seg._judge_seal({"cell": cell15b, "hit": true}, 1)
var beacon_row := {}
for r in rows15b:
if String(r.get("effect_id", "")) == "fx_preview_area":
beacon_row = r
Kit.check_quiet(not beacon_row.is_empty() and String(beacon_row.get("type", "")) == "extended"
and String(beacon_row.get("source_class", "")) == "fx_warning_beacon",
"示警烟火命中行:type=extended+effect_id=fx_preview_area+来源类别=fx_warning_beacon(空串违约修复)〔明细收敛 V2〕")
quit(Kit.finish("treasure_effects_test", "PASS — 新目录 12 件逐件行为断言全绿〔秘宝批 V7 附属表〕"))