iter56-卡2:封印动作改版(迭代56卡2·受理即揭示): 规则核心(target_sealed挖封印格拒/清图口径=挖开∪封印/能清图的封最优先/notify_cleared清图收轮/sealed_identity撤出→BoardData.cell_identity——上限20用户裁)+结算面dug_under_seal删净+破封一击退役+呈现层(地板+身份字形+印记不依赖揭示态+文案四件)+探针V8-V10+sim域(标空封印剔除约束增强+错触直挖补偿注入0.10)+golden三图换代; MC同批重锚50/档=100局(2026-11-08用户批准)三方对照无结构性恶化; 高档审计通过(17行全量·审计后勘误2项闭环)

This commit is contained in:
DKChan committed 2026-11-08 03:47:33 +08:00
1 parent 908e518306
commit 5ec4a311bb
34 files changed
+589 -164

No files matched your search

@@ -48,6 +48,7 @@ func _init() -> void:
# ---- 末点 AP 封印轮末结算(playtest 缺陷②回归:末点 AP 花在封印必须终结轮次进入下一轮)----
func _smoke_seal_ap_exhausted() -> void:
# 判据出处: design/gdd/systems/战斗循环/战斗结算.md:139 封印判定批与溢价兑付链照常(保留组——封印受理即揭示改版零改动)〔封印改 V5〕
# 判据出处: design/gdd/systems/排雷循环/行动经济.md:260 (末点 AP 耗尽即收轮组)
print("--- 排雷玩法·封印动作耗尽末点 AP 回归 ---")
var rng = load("res://core/rng_service.gd").new()
@@ -111,11 +112,11 @@ func _find_seal_target(minesweeper, want_mine: bool) -> Dictionary:
# ---- 封印不可逆四情况冒烟(禁撤封印 + 挖封印格兑付改判)----
# 四情况:① 插危险格不动 = 标中奖励照常;② 插危险格再挖开 = 只受挖雷一层惩罚(mine_hit 照常)标中不发;
# ③ 插安全格不动 = 标空惩罚照常;④ 插安全格再挖开 = 零惩罚零奖励(正向兑付作废、标空不判)。
# 四情况(封印受理即揭示改版改判〔封印改 V2〕):① 插危险格不动 = 标中奖励照常;② 插危险格再挖开 = 挖封印格被拒(零动作零惩罚);
# ③ 插安全格不动 = 标空惩罚照常;④ 插安全格再挖开 = 挖封印格被拒(零动作——标空判定照发)。
# 拒绝面:seal_pull 出目录(out_of_catalog);重复封印仍 seal_present 拒绝。
# 场景 A(④ + 拒绝面):标安全格 → seal_pull 拒绝 → 重复封印拒绝 → 直接挖开 → 收轮。
# 场景 B(②):标危险格 → 直接挖开 → 收轮。
# 场景 A(④ 改判〔封印改 V2〕+ 拒绝面):标安全格 → seal_pull 拒绝 → 重复封印拒绝 → 挖封印格被拒 → 收轮(标空判定照发)。
# 场景 B(② 改判〔封印改 V2〕):标危险格 → 挖封印格被拒 → 收轮(标中判定照发)。
# 场景 C(①③):标安全格 + 标危险格 → 不挖收轮 → 标空/标中判定照常。
@@ -440,6 +441,7 @@ func _smoke_refresh_provenance_guard() -> void:
func _smoke_polarity_routing() -> void:
# 判据出处: design/gdd/systems/战斗循环/战斗结算.md:139 封印判定批与溢价兑付链照常(保留组——封印受理即揭示改版零改动)〔封印改 V5〕
# 判据出处: design/gdd/systems/战斗循环/战斗结算.md:366 (极性路由:正向只兑正向、负向只兑负向组)
# 判据出处: design/gdd/systems/战斗循环/战斗结算.md:366 (极性路由:正向只兑正向、负向只兑负向组)
print("--- 极性路由(挖安全格正向 / 挖危险格负向 / 标中正向 / 标空负向)---")
@@ -572,11 +574,11 @@ func _smoke_polarity_routing() -> void:
# ---- 封印不可逆四情况冒烟(禁撤封印 + 挖封印格兑付改判)----
# 四情况:① 插危险格不动 = 标中奖励照常;② 插危险格再挖开 = 只受挖雷一层惩罚(mine_hit 照常)标中不发;
# ③ 插安全格不动 = 标空惩罚照常;④ 插安全格再挖开 = 零惩罚零奖励(正向兑付作废、标空不判)。
# 四情况(封印受理即揭示改版改判〔封印改 V2〕):① 插危险格不动 = 标中奖励照常;② 插危险格再挖开 = 挖封印格被拒(零动作零惩罚);
# ③ 插安全格不动 = 标空惩罚照常;④ 插安全格再挖开 = 挖封印格被拒(零动作——标空判定照发)。
# 拒绝面:seal_pull 出目录(out_of_catalog);重复封印仍 seal_present 拒绝。
# 场景 A(④ + 拒绝面):标安全格 → seal_pull 拒绝 → 重复封印拒绝 → 直接挖开 → 收轮。
# 场景 B(②):标危险格 → 直接挖开 → 收轮。
# 场景 A(④ 改判〔封印改 V2〕+ 拒绝面):标安全格 → seal_pull 拒绝 → 重复封印拒绝 → 挖封印格被拒 → 收轮(标空判定照发)。
# 场景 B(② 改判〔封印改 V2〕):标危险格 → 挖封印格被拒 → 收轮(标中判定照发)。
# 场景 C(①③):标安全格 + 标危险格 → 不挖收轮 → 标空/标中判定照常。
func _smoke_seal_irreversible_quadrants() -> void:
# 判据出处: design/gdd/systems/排雷循环/排雷玩法.md:231 (封印不可逆四情况组)
@@ -600,9 +602,10 @@ func _smoke_seal_irreversible_quadrants() -> void:
Kit.check("四情况 A:seal_pull 出目录拒绝(撤封印退役)", not pull_out.is_empty() and not bool(pull_out[0].get("accepted", true)) and String(pull_out[0].get("sub_reason", "")) == "out_of_catalog")
var dup_place: Array = ms_a.settle_batch([{"action": "seal_place", "cell": t_a.seal_cell}])
Kit.check("四情况 A:重复封印仍被拒(seal_present)", not dup_place.is_empty() and not bool(dup_place[0].get("accepted", true)) and String(dup_place[0].get("sub_reason", "")) == "seal_present")
# 改判〔封印改 V2〕——「挖封印格直挖受理/隐式退役」随封印受理即揭示退役:挖已封印格被拒
var dig_a: Array = ms_a.settle_batch([{"action": "dig", "cell": t_a.seal_cell}])
Kit.check("四情况 A:封印格直接挖开被受理(不再须先撤封印)", not dig_a.is_empty() and bool(dig_a[0].get("accepted", false)))
Kit.check("四情况 A:封印随挖掘隐式退役(has_seal = false)", not ms_a.has_seal(t_a.seal_cell))
Kit.check("四情况 A:挖已封印格被拒且原因可辨(target_sealed)", not dig_a.is_empty() and not bool(dig_a[0].get("accepted", true)) and String(dig_a[0].get("sub_reason", "")) == "target_sealed")
Kit.check("四情况 A:封印不随挖掘退役(has_seal 保持——不可再挖开)", ms_a.has_seal(t_a.seal_cell) and not ms_a.is_revealed(t_a.seal_cell))
ms_a.settle_batch([{"action": "stop"}])
var target_a: Array = [t_a.seal_cell.x, t_a.seal_cell.y]
var miss_a := 0
@@ -617,9 +620,11 @@ func _smoke_seal_irreversible_quadrants() -> void:
grant_a += 1
if row.get("type", "") == "reward_ineffective":
void_a += 1
Kit.check("四情况 A:标空判定零兑付(×%d——封印已擦除不入轮末基线)" % miss_a, miss_a == 0)
Kit.check("四情况 A:安全格正向兑付作废(×%d——零奖励)" % grant_a, grant_a == 0)
Kit.check("四情况 A:兑付作废归因行恰一次(×%d——零惩罚零奖励留痕)" % void_a, void_a == 1)
# 改判〔封印改 V2〕——封印在场入轮末基线:标空判定照发(受理即揭示不改判定批);
# miss_a 口径=seal_miss 型行或来源行(判定行+标空负向兑付行同源)
Kit.check("四情况 A:标空判定照发(×%d——封印在轮末基线;判定行+负向兑付行同源)" % miss_a, miss_a >= 1)
Kit.check("四情况 A:安全格正向兑付为零(×%d——封印不兑付格面资源)" % grant_a, grant_a == 0)
Kit.check("四情况 A:零兑付作废归因行(×%d——改判路径退役无作废行)" % void_a, void_a == 0)
# 场景 B:危险格直挖(情况②)
var rng_b = load("res://core/rng_service.gd").new()
rng_b.set_master_seed(20260919)
@@ -634,9 +639,10 @@ func _smoke_seal_irreversible_quadrants() -> void:
if t_b.hero_cell != ms_b.hero_pos():
ms_b.settle_batch([{"action": "move", "cell": t_b.hero_cell}])
Kit.check("四情况 B:封印受理", ms_b.settle_batch([{"action": "seal_place", "cell": t_b.seal_cell}])[0].accepted)
# 改判〔封印改 V2〕——挖危险封印格同被拒(封印格拒绝挖掘不分极性)
var dig_b: Array = ms_b.settle_batch([{"action": "dig", "cell": t_b.seal_cell}])
Kit.check("四情况 B:封印危险格直接挖开被受理", not dig_b.is_empty() and bool(dig_b[0].get("accepted", false)))
Kit.check("四情况 B:危险格已揭示(挖雷本体照常)", ms_b.is_revealed(t_b.seal_cell) and ms_b.current_board().is_mine(t_b.seal_cell))
Kit.check("四情况 B:挖已封印危险格被拒(target_sealed)", not dig_b.is_empty() and not bool(dig_b[0].get("accepted", true)) and String(dig_b[0].get("sub_reason", "")) == "target_sealed")
Kit.check("四情况 B:危险格未揭示(封印格零动作)", not ms_b.is_revealed(t_b.seal_cell) and ms_b.current_board().is_mine(t_b.seal_cell))
ms_b.settle_batch([{"action": "stop"}])
var target_b: Array = [t_b.seal_cell.x, t_b.seal_cell.y]
var mine_hit_b := 0
@@ -648,8 +654,9 @@ func _smoke_seal_irreversible_quadrants() -> void:
mine_hit_b += 1
if row.get("type", "") == "seal_hit" or String(row.get("source_class", "")) == "seal_hit":
hit_b += 1
Kit.check("四情况 B:挖雷惩罚照常兑付(×%d——只受挖雷一层惩罚)" % mine_hit_b, mine_hit_b >= 1)
Kit.check("四情况 B:标中不发(×%d——封印已擦除不判)" % hit_b, hit_b == 0)
# 改判〔封印改 V2〕——封印在场入轮末基线:标中判定照发(含溢价兑付链)
Kit.check("四情况 B:挖雷惩罚零发生(×%d——封印格未挖)" % mine_hit_b, mine_hit_b == 0)
Kit.check("四情况 B:标中判定照发(×%d——封印在轮末基线)" % hit_b, hit_b >= 1)
# 场景 C:不挖收轮(①③判定照常)
var rng_c = load("res://core/rng_service.gd").new()
rng_c.set_master_seed(20260919)
@@ -420,6 +420,8 @@ var _db
func _init() -> void:
# 判据出处: design/gdd/systems/战斗循环/战斗结算.md:362 (结算接线同种子逐项 golden 对拍组)
# 判据出处: design/gdd/systems/战斗循环/战斗结算.md:134 受理零兑付、改判路径死——流水零「奖励作废(封印格直接挖开)」行、
# 同种子驱动序 golden 实测零漂移零重录(挖封印格被拒——驱动序不含该路径)〔封印改 V6〕
_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)
+10 -12
View File
@@ -147,9 +147,11 @@ func _smoke_adventure() -> void:
Kit.check("生成图每个战斗/Boss 节点经解析链命中敌行集与棋盘行(扫描 %d 节点,缺失 %d)〔fv24〕" % [cov_scan, cov_miss],
cov_scan > 0 and cov_miss == 0)
# ② 满配冒险成功 + ④ 三层奖励兑付(三选一恰 2 次 · 回血封顶 · 钥石逐杀 · 候选零重复)
var st: Dictionary = autoplay.run_adventure(_db, 42, 900, 400)
# 种子换代〔退役改 V7 波及面——test-conventions §5.5 同生死〕:feats.tres 删破封一击行使
# RNG 流消费序列变,42 路线漂移为 timeout(43 实跑 completed——钉值随表行集换代)
var st: Dictionary = autoplay.run_adventure(_db, 43, 900, 400)
var sum: Dictionary = st["summary"]
Kit.check("满配冒险成功(42 / atk+900 / hp+400——cleared=true;v2 怪阶梯后旧 200/100 档不再满配)", String(st["result"]) == "completed" and bool(sum.get("cleared", false)))
Kit.check("满配冒险成功(43 / atk+900 / hp+400——cleared=true;v2 怪阶梯后旧 200/100 档不再满配)", String(st["result"]) == "completed" and bool(sum.get("cleared", false)))
Kit.check("完成几关 = 3(levels_cleared)", int(sum.get("levels_cleared", -1)) == 3)
var ks_exp := 0
for li in range(1, int(_db.run.level.rows.size()) + 1):
@@ -157,7 +159,7 @@ func _smoke_adventure() -> void:
Kit.check("钥石合计 = ΣBoss 首杀行值(%d——逐杀掉落新口径;断点未注入恒首杀)" % int(sum.get("keystone_total", -1)), int(sum.get("keystone_total", -1)) == ks_exp)
var drops: Array = sum.get("keystone_drops", []) as Array
Kit.check("逐杀掉落明细恰 3 笔(L1/L2/L3——additive 摘要键 keystone_drops)", drops.size() == 3)
Kit.check("摘要携带主种子(42)", int(sum.get("master_seed", -1)) == 42)
Kit.check("摘要携带主种子(43)", int(sum.get("master_seed", -1)) == 43)
var picks: Array = []
var resolves: Array = []
var node_picks := 0
@@ -274,7 +276,7 @@ func _smoke_adventure() -> void:
seq_ok = false
Kit.check("各战上限入参 ∈ [基础, 基础+3×件值+购血余量](覆盖值通道——双渠道共享上限 3;模拟档贪心购血合法叠加〔数值修复批 v3.4〕;首战=基础 %d)" % exp_base, seq_ok)
# ③ 种子域可复现(同种子两局:summary + 上限序列 + 行进序列 + 三选一停点载荷逐位一致)
var st2: Dictionary = autoplay.run_adventure(_db, 42, 900, 400)
var st2: Dictionary = autoplay.run_adventure(_db, 43, 900, 400)
Kit.check("同种子两局逐位可复现(summary/上限序列/行进序列/三选一载荷——reward_pick 封闭流)",
st2["summary"] == sum and st2["encounter_max_hp_seq"] == seq
and st2["visited_ids"] == st["visited_ids"] and st2["reward_events"] == st["reward_events"])
@@ -465,8 +467,8 @@ func _smoke_adventure() -> void:
func _smoke_feats() -> void:
# 判据出处: design/gdd/systems/冒险循环/奇物.md:465 (奇物授予与凭证保底组)
print("--- 专长重设计实装(feat_table 四级 · 专长带池接线 · 订阅面逐件)---")
# ① 现行表牌库构成:37 件全量入池——102 数值令牌+37 relic 令牌(四级 rarity 直映射)
Kit.check("feat_table 已加载(37 件=基础 27+迁入 2+完美保险 1+迭代 46 换图系七件——换代)", _db.crossrun.feats != null and (_db.crossrun.feats.rows as Array).size() == 37)
# ① 现行表牌库构成:36 件全量入池(破封一击随封印受理即揭示退役〔退役改 V7〕)——102 数值令牌+36 relic 令牌(四级 rarity 直映射)
Kit.check("feat_table 已加载(36 件=基础 27+迁入 2+完美保险 1+迭代 46 换图系七件−破封一击退役〔退役改 V7〕——换代)", _db.crossrun.feats != null and (_db.crossrun.feats.rows as Array).size() == 36)
Kit.check("feat_table.validate 零违例(四级 rarity/cap 域/trigger 词表/行键白名单全过)", _db.crossrun.feats.validate().is_empty())
var a_r0 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(71))
Kit.check("专长带场景:冒险启动(seed 71)", a_r0.start_adventure(71))
@@ -477,7 +479,7 @@ func _smoke_feats() -> void:
feat_tokens += 1
var tk := String((t as Dictionary).get("tier", ""))
tier_counts[tk] = int(tier_counts.get(tk, 0)) + 1
Kit.check("专长带入池:37 件四级直映射(普通 14/优秀 13/稀有 10/传说 0)——牌库 102+37=139〔专长批 V2+迭代 46 换图系七件换代〕", feat_tokens == 37 and int(tier_counts.get("common", 0)) == 14 and int(tier_counts.get("excellent", 0)) == 13 and int(tier_counts.get("rare", 0)) == 10 and a_r0.deck().deck.size() == 139)
Kit.check("专长带入池:36 件四级直映射(普通 14/优秀 12/稀有 10/传说 0)——牌库 102+36=138〔专长批 V2+迭代 46 换图系七件换代−破封一击退役〕", feat_tokens == 36 and int(tier_counts.get("common", 0)) == 14 and int(tier_counts.get("excellent", 0)) == 12 and int(tier_counts.get("rare", 0)) == 10 and a_r0.deck().deck.size() == 138)
# ② 旧 27 件零残留钉面(V2——arcane_ember 系旧件表首行;全仓 grep 零残留归静态核查)
Kit.check("旧 27 件已退役:arcane_ember/magma_body 表内零残留〔专长批 V2〕", _db.crossrun.feats.row_by_id("arcane_ember").is_empty() and _db.crossrun.feats.row_by_id("magma_body").is_empty())
Kit.check("迁出收尾:treasure_items 无 venom_edge/seal_burst 行(同调目录自然收窄)〔专长批 V13〕", _db.crossrun.treasure_items.row_by_id("venom_edge").is_empty() and _db.crossrun.treasure_items.row_by_id("seal_burst").is_empty())
@@ -717,11 +719,7 @@ func _smoke_feats() -> void:
Kit.check("背水:生命≤50% 减伤·过半血反例零减伤〔专长批 V3〕",
_hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "damage_taken", {"kind": "enemy_skill", "amount": 5, "source": "s", "round": 1, "hp_ratio": 40}), "damage_reduce").size() == 1
and _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "damage_taken", {"kind": "enemy_skill", "amount": 5, "source": "s", "round": 2, "hp_ratio": 80}), "damage_reduce").is_empty())
# 跨族桥
fx.rebuild(["breaker_strike"], [], {})
Kit.check("破封一击:挖开带封印危险格 +1 层·无封印反例零触发〔专长批 V3〕",
_hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {"dug_under_seal": true}), "counter_add").size() == 1
and _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {"dug_under_seal": false}), "counter_add").is_empty())
# 跨族桥(破封一击随封印受理即揭示改版退役——dug_under_seal 事实词除名,改判路径不存在〔封印改 V7〕)
# 节奏系
fx.rebuild(["map_exchange"], [], {})
var me_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "dig_stop", {"ap_left": 4})
+4 -2
View File
@@ -21,10 +21,12 @@ func _init() -> void:
Kit.check_quiet(EffectEngineScript.EFFECT_KINDS.has("grant_reserve_ap") and EffectEngineScript.EFFECT_KINDS.has("apply_discount"),
"换图系新效果在册:grant_reserve_ap/apply_discount〔换图系批 V1〕")
Kit.check_quiet(EffectEngineScript.FACTS.has("hp_ratio") and EffectEngineScript.FACTS.has("is_edge_cell")
and EffectEngineScript.FACTS.has("new_resource_type") and EffectEngineScript.FACTS.has("dug_under_seal")
and EffectEngineScript.FACTS.has("new_resource_type")
and EffectEngineScript.FACTS.has("ap_exhausted_last") and EffectEngineScript.FACTS.has("ap_left")
and EffectEngineScript.FACTS.has("is_blank_zero") and EffectEngineScript.FACTS.has("amount"),
"新事实在册:hp_ratio/is_edge_cell/new_resource_type/dug_under_seal/ap_exhausted_last/ap_left/is_blank_zero/amount〔专长批 V1〕")
"新事实在册:hp_ratio/is_edge_cell/new_resource_type/ap_exhausted_last/ap_left/is_blank_zero/amount〔专长批 V1——dug_under_seal 随破封一击退役除名,退役改 V7〕")
Kit.check_quiet(not EffectEngineScript.FACTS.has("dug_under_seal"),
"退役事实词出册:dug_under_seal(改判路径退役——词表除名防回潮)〔退役改 V7〕")
Kit.check_quiet(EffectEngineScript.EFFECT_KINDS.has("heal") and EffectEngineScript.EFFECT_KINDS.has("ac_bonus")
and EffectEngineScript.EFFECT_KINDS.has("damage_reduce") and EffectEngineScript.EFFECT_KINDS.has("bonus_delta")
and EffectEngineScript.EFFECT_KINDS.has("blast_scaled") and EffectEngineScript.EFFECT_KINDS.has("preview_area")
+169 -2
View File
@@ -42,6 +42,10 @@ func _init() -> void:
_test_seal_place()
_test_reject_out_of_catalog()
_test_preview_reveal_readonly()
_test_seal_reveal_identity()
_test_dig_sealed_rejected()
_test_clear_by_seal()
_test_classify_seal_clears()
quit(Kit.finish("minesweeper_rules_test"))
@@ -133,12 +137,15 @@ func _test_preview_reveal_readonly() -> void:
Kit.check("V14 形态A:preview 返回集(真板形态——%d 格)" % prev_a.size(), prev_a.size() >= 0)
Kit.check("V14 形态A:调用前后零状态写入(revealed/seal 计数零变化)〔架构整改 V14〕",
_rules.revealed_cells().size() == before_revealed and _rules.seal_count() == before_seals)
# 形态 B:挖封印格——本格封印隐式退役只入模拟集,真实封印计数不动
# 形态 B(改判〔封印改 V14B〕——挖封印格隐式退役前提随「封印受理即揭示」退役):
# preview(封印格) 连锁不越封印——封印格自身零外扩返回空集,真实封印计数不动
var seal_c := _unrevealed_safe()
_rules.settle_batch([{"action": "seal_place", "cell": seal_c}])
var sealed_before := _rules.seal_count()
var prev_b: Array = _rules.preview_reveal(seal_c)
Kit.check("V14 形态B:preview(封印格) 零状态写入(seal_count 不变——隐式退役只入模拟集)〔架构整改 V14〕",
Kit.check("V14 形态B:preview(封印格) 零外扩(连锁不越封印——返回空集 %d 格)〔封印改 V14B〕" % prev_b.size(),
prev_b.is_empty())
Kit.check("V14 形态B:preview(封印格) 零状态写入(seal_count 不变)〔封印改 V14B〕",
_rules.seal_count() == sealed_before)
# 同口径对照:preview(c) = settle_batch(dig c) 后 revealed 差集 ∖ {c}(F2——不含首格与已开格)
var dig_c := _unrevealed_safe()
@@ -159,3 +166,163 @@ func _test_preview_reveal_readonly() -> void:
var _pc: Array = _rules.preview_reveal(_unrevealed_safe())
Kit.check("V14 零事件:preview 全程 cell_revealed 零发射(前后增量 0——实测 %d→%d)〔架构整改 V14〕" % [events_before_previews, events],
events == events_before_previews)
# 未完成安全格集(清图口径=已挖∪已封印——未揭且未封;V3 阶段c/V4 夹具扫描口)
func _incomplete_cells() -> Array:
var out: Array = []
for c in _board.safe_cells():
if not _rules.is_revealed(c) and not _rules.has_seal(c):
out.append(c)
return out
# 新板重装夹具(V3 阶段c/V4 干净局面——IDLE 装板→round_start 进 ACTIVE;封印/揭示随装板重置)
func _fresh_board(seed_v: int) -> void:
_ae._phase = 0 # Phase.IDLE——夹具直置(前置相位可能处 ACTIVE:fx_reveal 不经行动经济不收轮;notify_combat_end 相位守卫拒 ACTIVE)
var rng := RngServiceScript.new()
rng.set_master_seed(seed_v)
rng.new_adventure(seed_v)
_board = BoardGeneratorScript.new().generate(1, 0, rng.open_stream("board_gen"), _db)
_rules.install_board(_board)
_ae.notify_round_start()
# ---- 封印受理即揭示改版(迭代 56 封印动作改版批 V1–V4——身份可查/挖封印格被拒/清图口径/批次语义序)----
# 未揭示且未封印的安全格(V1-V4 夹具对象选择口——排除已封印格)
func _unrevealed_unsealed_safe() -> Vector2i:
for c in _board.safe_cells():
if not _rules.is_revealed(c) and not _rules.has_seal(c):
return c as Vector2i
return Vector2i(-1, -1)
# V1 封印受理即揭示:身份当场可查(sealed_identity 与板事实逐位一致)、受理零事件零兑付
func _test_seal_reveal_identity() -> void:
# 判据出处: design/gdd/systems/排雷循环/排雷玩法.md:52 封印受理那一刻身份当场可见、受理当场零兑付零事件
_ae.grant_ap(20)
_rules.seal_range_free = true
var safe_c := _unrevealed_unsealed_safe()
var events := 0
_rules.cell_revealed.connect(func(_evt): events += 1) # 受理零事件哨兵
var events_before := events
_rules.settle_batch([{"action": "seal_place", "cell": safe_c}])
Kit.check("V1 前提:安全格封印受理", _rules.has_seal(safe_c))
# 身份揭示拼装(用户裁 2026-11-08 上限保持 20——sealed_identity 撤出 rules,
# 身份事实=BoardData.cell_identity、封印态=has_seal,两段各归其主)
Kit.check("V1 安全格封印身份 kind=safe 且 digit=板数字逐位一致(%s vs %d)〔封印改 V1〕" % [str(_board.cell_identity(safe_c).get("kind", "")), _board.digit_of(safe_c)],
_rules.has_seal(safe_c) and String(_board.cell_identity(safe_c).get("kind", "")) == "safe"
and int(_board.cell_identity(safe_c).get("digit", -9)) == _board.digit_of(safe_c))
Kit.check("V1 受理零事件:cell_revealed 零发射(%d→%d)〔封印改 V1〕" % [events_before, events],
events == events_before)
# 危险格封印身份:找未开危险格封印 → kind=mine
var mine_c := Vector2i(-1, -1)
for c in _board.all_cells():
if _board.is_mine(c) and not _rules.is_revealed(c) and not _rules.has_seal(c):
mine_c = c
break
if mine_c != Vector2i(-1, -1):
_rules.settle_batch([{"action": "seal_place", "cell": mine_c}])
Kit.check("V1 危险格封印身份 kind=mine〔封印改 V1〕",
_rules.has_seal(mine_c) and String(_board.cell_identity(mine_c).get("kind", "")) == "mine")
Kit.check("V1 身份事实与揭示态独立:未封印格 has_seal=false 但身份可查(板事实恒在)〔封印改 V1〕",
not _rules.has_seal(_unrevealed_unsealed_safe())
and not _board.cell_identity(_unrevealed_unsealed_safe()).is_empty())
# V2 挖已封印格被拒:原因可辨 target_sealed、封印/揭示/AP 三态零变化
func _test_dig_sealed_rejected() -> void:
# 判据出处: design/gdd/systems/排雷循环/排雷玩法.md:56 动作合法性表——挖的拒绝情形含已封印
var sealed_c := Vector2i(-1, -1)
for c in _board.all_cells():
if _rules.has_seal(c) and not _rules.is_revealed(c):
sealed_c = c
break
Kit.check("V2 前提:已封印未开格在板", _rules.has_seal(sealed_c) and not _rules.is_revealed(sealed_c))
_rules.dig_range_bonus = 99 # 夹具免邻接(公开属性直置——seal_range_free 先例同款;挖掘射程非判据面)
_ae.grant_ap(20)
var ap_before: int = _ae._ap
var out: Array = _rules.settle_batch([{"action": "dig", "cell": sealed_c}])
var o: Dictionary = (out[0] as Dictionary) if not out.is_empty() else {}
Kit.check("V2 挖封印格被拒且原因可辨(sub_reason=target_sealed——实测 %s)〔封印改 V2〕" % String(o.get("sub_reason", "")),
String(o.get("outcome", "")) == "rejected" and String(o.get("sub_reason", "")) == "target_sealed")
Kit.check("V2 拒绝零副作用:封印态/揭示态/AP 三态零变化〔封印改 V2〕",
_rules.has_seal(sealed_c) and not _rules.is_revealed(sealed_c) and _ae._ap == ap_before)
_rules.dig_range_bonus = 0 # 还原射程(后续夹具不吃豁免)
# V3 清图口径换代:封印可完成清图(封掉最后安全格→cleared 收束);非末封印不清图
func _test_clear_by_seal() -> void:
# 判据出处: design/gdd/systems/排雷循环/排雷玩法.md:139 清图=全部安全格完成,完成=挖开或封印
_ae.grant_ap(20)
_rules.seal_range_free = true
# 阶段 a:封一张非末安全格 → clear_map_cleared 仍 false(封印计入完成但未清完)
var seal_c := _unrevealed_unsealed_safe()
_rules.settle_batch([{"action": "seal_place", "cell": seal_c}])
Kit.check("V3 阶段a 前提:封印受理", _rules.has_seal(seal_c))
Kit.check("V3 阶段a:非末封印后清图未达成(clear_map_cleared=false)〔封印改 V3〕",
not _rules.clear_map_cleared())
# 阶段 b:fx_reveal 除 seal_c 外全部未完成安全格 → 唯一未完成格=已封印的 seal_c → 封印计入完成
for c in _board.safe_cells():
if c != seal_c and not _rules.is_revealed(c) and not _rules.has_seal(c):
_rules.fx_reveal(c)
Kit.check("V3 阶段b:唯一未完成格=已封印格 → clear_map_cleared=true〔封印改 V3〕",
_rules.clear_map_cleared())
# 阶段 c:另起干净新板——逐格封至清图(每封一批次真实入口):非末封零 round_end、
# 末封恰发一条 path_tag=cleared 的轮末事件(封印后清图判定触发——轮末事件 carry 清图)
_fresh_board(81003)
_ae.grant_ap(60)
_rules.seal_range_free = true
var end_seen: Array = []
_rules.round_end_board.connect(func(p): end_seen.append(String(p.get("path_tag", ""))))
var guard := 0
while not _rules.clear_map_cleared() and guard < 200:
guard += 1
var t: Vector2i = _incomplete_cells()[0] if not _incomplete_cells().is_empty() else Vector2i(-1, -1)
if t == Vector2i(-1, -1):
break
_rules.settle_batch([{"action": "seal_place", "cell": t}])
Kit.check("V3 阶段c:逐格封至清图达成(%d 封——封印可完成清图)〔封印改 V3〕" % guard,
_rules.clear_map_cleared())
Kit.check("V3 阶段c:清图收束恰一次且 path_tag=cleared(round_end 序列=%s)〔封印改 V3〕" % str(end_seen),
end_seen.size() == 1 and String(end_seen[0]) == "cleared")
# V4 批次语义序:同批「停手 + 能清图的封」提交 → 赢家=清图收束而非停手
func _test_classify_seal_clears() -> void:
# 判据出处: design/gdd/systems/排雷循环/排雷玩法.md:167 最先=能清图的挖或封——防耗尽优先把清图高光结算成最差结局
# 收敛未完成格到恰 1 格(封它即清图;fx_reveal 连锁不可控——逐格揭至剩 1,连锁越格则换种子重试)
var left: Array = []
var last_c := Vector2i(-1, -1)
for seed_i in range(3):
_fresh_board(81004 + seed_i)
_ae.grant_ap(60)
_rules.seal_range_free = true
left = _incomplete_cells()
var guard2 := 0
while left.size() > 1 and guard2 < 400:
_rules.fx_reveal(left[0])
left = _incomplete_cells()
guard2 += 1
if left.size() == 1:
last_c = left[0]
break
Kit.check("V4 前提:未完成格收敛到恰 1 格(实测 %d——封它即清图)" % left.size(),
left.size() == 1)
if left.size() != 1:
quit(Kit.finish("minesweeper_rules_test"))
return
var intents: Array = [{"action": "stop"}, {"action": "seal_place", "cell": last_c}]
# 同批「停手+能清图的封」:末格封 cls 0 最先执行→cleared 收轮;stop cls 1 voided
var end_seen: Array = []
_rules.round_end_board.connect(func(p): end_seen.append(String(p.get("path_tag", "")))) # append 捕获引用(lambda 内重绑定不回写)
var outcomes: Array = _rules.settle_batch(intents)
Kit.check("V4 赢家=清图封:round_end path_tag=cleared 而非 stopped(round_end 序列=%s)〔封印改 V4〕" % str(end_seen),
end_seen.size() == 1 and String(end_seen[0]) == "cleared")
Kit.check("V4 清图封印受理(末格 %s 在封印态)〔封印改 V4〕" % str(last_c), _rules.has_seal(last_c))
# outcomes 按执行序承载(cls 升序)——赢家=cls 0 的清图封居首;stop 与非末封在其后 voided
var o_winner: Dictionary = (outcomes[0] as Dictionary) if not outcomes.is_empty() else {}
Kit.check("V4 末格封意图受理(batch 赢家=清图封居执行序首——实测 %s)〔封印改 V4〕" % String(o_winner.get("outcome", "")),
String(o_winner.get("outcome", "")) == "executed")
Kit.check("V4 其余意图 voided(清图收轮后批次余项作废)〔封印改 V4〕",
outcomes.size() >= 2 and String((outcomes[outcomes.size() - 1] as Dictionary).get("outcome", "")) == "voided")
@@ -18,6 +18,11 @@ func check(cond: bool, msg: String) -> void:
func _init() -> void:
# 判据出处: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
_smoke_settle_zero_rows()
func _smoke_settle_zero_rows() -> void:
# 判据出处: design/gdd/08-Alpha版本标准.md:154 战斗演出链(弹道 / 受击 / 血条 / 灰化)在役;
# 逐行结算排播行已随 v1.29 退役——结算期 B 区零逐行文字、FX 排播链保持
var main: Node = load("res://presentation/main.tscn").instantiate()
@@ -27,7 +27,8 @@ func _test_reject_text_by_reason() -> void:
var cases := {
"not_adjacent": "不能执行:目标与小人不相邻",
"target_revealed": "不能执行:目标已开",
"seal_present": "不能封印:此格已有封印印记(封印为一次性承诺——相邻时左键直接挖开)",
"seal_present": "不能封印:此格已有封印印记(封印受理即揭示——印记随图存续)",
"target_sealed": "不能挖:此格已封印(封印受理即揭示——不可再挖开)",
}
var bad: Array[String] = []
for sub in cases:
@@ -76,7 +76,10 @@ func _sweep_legend() -> void:
seal_row = e
break
Kit.check("图例封印行含「标空」(u4——与「标中」成对)〔wa5〕", String(seal_row[2] if seal_row.size() > 2 else "").contains("标空"))
Kit.check("图例封印行含「受挖掘惩罚」(u4 雷字清零换代)〔wa5〕", String(seal_row[2] if seal_row.size() > 2 else "").contains("受挖掘惩罚"))
# 文案换代〔封印改 V10〕——图例封印行新口径:受理即揭示、已封印格零动作(旧「受挖掘惩罚」句随改判路径退役)
Kit.check("图例封印行含「受理即揭示」且零雷字(u4 清零换代〔封印改 V10〕)〔wa5〕",
String(seal_row[2] if seal_row.size() > 2 else "").contains("受理即揭示")
and not String(seal_row[2] if seal_row.size() > 2 else "").contains("雷"))
for w in ["插错", "挖雷"]:
Kit.check("图例封印行零「%s」〔wa5〕" % w,
not String(seal_row[2] if seal_row.size() > 2 else "").contains(w))
@@ -14,6 +14,7 @@ func _init() -> void:
_test_param_values()
_test_shader_uniforms()
_test_fog_gate()
_test_seal_on_revealed_visible()
quit(Kit.finish("seal_mark_params_test"))
@@ -54,3 +55,16 @@ func _test_fog_gate() -> void:
u.seal_on = false
Kit.check("撤 seal_on 即撤子件〔seal批 V4〕", u._seal_layer == null)
u.free()
# R8 新组合态:seal_on 与已揭示同真时印记子件仍可见(封印受理即揭示改版——印记可见性不依赖
# 揭示态;内部字段直查=受控例外同 _test_fog_gate——子件可见性无公开可观测面,对照表随行申报)
func _test_seal_on_revealed_visible() -> void:
# 判据出处: design/ux/battle.md「封印格呈现预览」受理即揭示——印记承载面换代(已揭示格承载合法)
var u: Node2D = CELL_UNIT.new()
u.seal_on = true
Kit.check("R8 前提:seal_on 挂封印子件〔封印改 V8〕", u._seal_layer != null)
u.fog_state = 0
Kit.check("R8 新组合态:seal_on 与揭示同真印记仍可见(非雾态——承载面不因揭示态撤印)〔封印改 V8〕",
u._seal_layer != null and u._seal_layer.visible == true)
u.free()