卡②(结构小件批p1-p7+V15): 七点定点修落地+运行期违约通道负面套件+wr10 偏运缝拆验收——①p1 状态角标判据去重(main_overlay_flow._sync_status_entry 一组重复赋值语句删除,battle_live 局部量随之退役=wr1 载体);②p2 确认窗开合归模态栈单口(main 两处 _on_confirm_* 改 _close_modal;存档删除入口改 root._open_modal(root.confirm_dialog) 后再 open,遮挡记账/挂起兜底/开侧同步同口);③p3 棋盘动作与 stop 两入口显式 settlement 空防护(锁谓词异常时不崩于空引用);④p4 会话更替即清队:_begin_session 首行 _row_pacer.flush()(待播行不住局态=机制保证非时序约定);⑤p5 缝拆:RewardEconomy 删跨域直持(_fx/_ctx/set_fx)改注入 luck_bias_query: Callable,reward_roll 发射与 bias_rewards 求和收回 BattleFxHost.luck_bias_for(cell_count)(三参形 dispatch,不带 facts——禁复用 fx_emit 的第四实参会打开 when 事实条件面),组装点 set_fx 一行换赋值一行 1 换 1;类变量 15−2+2 净 15,gdstyle 无第 16 枚;⑥p6/p7 RngService 未知用途标签与未播种取数两分支加 violation_handler 记录位+release 下 return null 空引用即刻崩(fail-fast 传导,合法路径不可达)——debug 下 assert 先中断函数本体两态不可判别,故记录位是唯一可观测面;⑦board_generator/reward_economy 各加 fatal_handler 注入位并由组装点接 orchestrator._fatal(release 剥 assert 后仍经 battle_result 封印真中止)。新件 tests/unit/core/run_time_fatal_test.gd(manifest 登记 expect=1 negative=true):wr5 协作件 _fatal 经组装点传导 p5 封印+缺省形态不外溢/wr6 未知标签通道/wr7 未播种通道(各用新建 RngService 实例,播种断言 _counter==0)。wr10 三条入 variant_placement_test=判定面夹具白盒装载(现网数据无 bias_rewards 行,沿 tree_rules_v11_test 先例)+接线同源+权重生效 77+4*2。验收:variant_placement 49 checks 全绿(wr10 ①②③ PASS:空订阅 0/命中 1 行/装载后 2;查询口有效且与宿主同源;偏运池实测 85);run_time_fatal_test 退出码 1 三点绿;pacing_queue 绿(wr4 前置夹具真入队 2 行→更替后清零);ui_click_probe 问题 0(wr2 遮挡账开/关两侧断言);round_flow_skill_pool 130 checks 绿;wr11 dump_battle 六组(L{1,2}×S{42,779401,779501})与改前 before_* 逐字节 cmp 全等;gdstyle 基线外新违规 0、恶化 1(level_map_screen 类变量 22>21=iter38 存量非本批触碰面)、基线文件零改动〔结构小件批 V15〕

This commit is contained in:
DKChan committed 2026-10-27 18:39:53 +08:00
1 parent 3a20a40891
commit ea322599f7
14 files changed
+211 -27

No files matched your search

+12 -1
View File
@@ -23,7 +23,7 @@ var damage: DamageController
var emit_cb: Callable # 宿主 _emit_row——行协议面单点(5 参 head 束形:head/ri/display/value/extra)
var facts_cb: Callable # 宿主 _fx_host_fact——round_index/battle_index/seals_count/nest_count/hp_ratio 五事实宿主侧单点
var grant_cb: Callable = Callable() # 揭示兑付回调=RewardGrant 授予面(bind_grant 注入;构造序解法——本件对授予面零强引用)
var engine: EffectEngine # 效果引擎(原编排器 _fx——宿主 _init 经此转注 damage/action_economy/reward_economy 三 set_fx)
var engine: EffectEngine # 效果引擎(原编排器 _fx——宿主 _init 经此转注 damage/action_economy 两 set_fx;reward_economy 经 luck_bias_for 查询口取回偏运合计)
var counters_ctx: EffectContext # 四桶引用束(round/battle 两桶本件自有;adventure/level 桶 begin_battle mount_buckets 挂引用)
var ac := {"base": -1, "round": 0, "battle": 0} # ac 束(base=主角 AC 基线快照 enc.hero_armor_class,-1=表值兜底未解析;round=ac_bonus 本轮累积器〔封印桩,round_start 清零〕;battle=本战累积器〔石肤,cap 封顶,begin_battle 清零〕)
var _fx_bonus_delta := {"atk": 0, "max_hp": 0, "armor_class": 0, "ap": 0} # 本战 bonus_delta 累积(战末宿主 bonus_delta_drain 读后清零)
@@ -55,6 +55,17 @@ func fx_emit(event: String, payload: Dictionary) -> Array:
return engine.dispatch(counters_ctx, event, payload, _fx_fact)
# 偏运权重查询(reward_roll 发射面——bias_rewards 词汇在本层收口,掷格域只见整数)。
# 三参形 dispatch(不带 facts:本事件订阅行 when 事实面不参与,命中集与缝拆前逐字同);
# 发射时点由掷格件在开流之后、权重装配之前调用;零命中=0=原行集零扰动。
func luck_bias_for(cell_count: int) -> int:
var total := 0
for h in engine.dispatch(counters_ctx, "reward_roll", {"polarity": "positive", "cell_count": cell_count}):
if String(h.get("kind", "")) == "bias_rewards":
total += int(h.get("value", 0))
return total
# round_start 发射+施加(grant_ap mode:queue=当轮入队,行动经济轮切换即兑现;计数器 round_start
# 重置点在 dispatch 内生效。调用点两处:begin_battle 首轮 / 轮始段续轮。payload 携 round〔宿主侧
# 事实单点〕+ap_exhausted_last〔一滴不剩 when 面〕;发射前 ac_bonus 本轮累积器清零并同步主角 AC)
+2 -1
View File
@@ -211,6 +211,7 @@ func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void:
_enemy_system = UnitContainer.new(db)
_status_effects = StatusContainer.new(db)
_reward_economy = RewardEconomy.new(db, rng)
_board_generator.fatal_handler = _fatal; _reward_economy.fatal_handler = _fatal # 协作件违约→本类 _fatal 封印(单点接线)
_damage = DamageController.new(db, rng, _enemy_system, _status_effects)
_damage.emit_row_cb = _emit_row # 行协议归属本类(9 类行协议 + 来源断言不外泄)
_fx_host = BattleFxHost.new({"db": db, "rng": rng, "minesweeper": minesweeper, "action_economy": action_economy,
@@ -221,7 +222,7 @@ func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void:
_fx_host.bind_grant(_reward_grant.reveal_redeem) # 构造序禁环:两件建齐后接线(fx 件对授予面走注入回调)
_damage.set_fx(_fx_host.engine, _fx_host.counters_ctx) # 效果引擎注入(attack_commit/clamp_check/armor_roll/kill/damage_taken 发射面)
action_economy.set_fx(_fx_host.engine, _fx_host.counters_ctx) # dig_paid/charge_full 发射面
_reward_economy.set_fx(_fx_host.engine, _fx_host.counters_ctx) # reward_roll 发射面
_reward_economy.luck_bias_query = _fx_host.luck_bias_for # 偏运查询口注入(reward_roll 发射归 fx 宿主)
minesweeper.cell_revealed.connect(_on_cell_revealed)
minesweeper.seal_judged.connect(_on_seal_judged)
minesweeper.round_end_board.connect(_on_round_end)
+6
View File
@@ -7,6 +7,10 @@ extends RefCounted
const METRIC_VERSION := 1
# 违约中止通道——缺省空 Callable 退化 push_error+assert(现行为逐字保持);组装层注入
# orchestrator._fatal,使 release 剥 assert 后仍经 battle_result 封印真中止。
var fatal_handler: Callable = Callable()
# ==================== 生成入口 ====================
@@ -395,4 +399,6 @@ func _lex_less(a: Array, b: Array) -> bool:
func _fatal(msg: String) -> void:
push_error("[棋盘生成/fatal] " + msg)
if fatal_handler.is_valid():
fatal_handler.call(msg) # 组装层注入 orchestrator._fatal(封印传导,不绕第二中止通道)
assert(false, msg)
+12 -14
View File
@@ -7,6 +7,9 @@ extends RefCounted
var db: ConfigDb
var rng: RngServiceCore
# 违约中止通道——缺省空 Callable 退化 push_error+assert(现行为逐字保持);组装层注入
# orchestrator._fatal,使 release 剥 assert 后仍经 battle_result 封印真中止。
var fatal_handler: Callable = Callable()
var _phase := "unrolled" # unrolled → ready → retired
var _eligible: Array[Dictionary] = [] # [{cell}] 行主序规范序
var _mapping := {} # Vector2i → reward_id(正向行身份——格面资源类型由 cell_type 承载)
@@ -16,14 +19,9 @@ var _granted := {} # Vector2i → 快照(重复受理幂等返
var _clear_identity := ""
var _clear_granted := false
var _clear_snapshot := {}
# 效果引擎(reward_roll 发射面;偏运硬币 bias_rewards 订阅面)
var _fx: EffectEngine = null
var _ctx: EffectContext = null
func set_fx(fx: EffectEngine, ctx: EffectContext) -> void:
_fx = fx
_ctx = ctx
# 偏运权重查询口(reward_roll 发射归 BattleFxHost 效果层——掷格域不持效果引擎/上下文;
# 参数=本次可掷格数、返回=偏运权重加算量、缺省空回调=恒 0=原行集零扰动)
var luck_bias_query: Callable = Callable()
func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void:
@@ -60,13 +58,11 @@ func roll_board(eligible: Array) -> bool:
_clear_granted = false
_clear_snapshot = {}
var stream: RandomNumberGenerator = rng.open_stream("reward_roll") # 恰一 scope 批(格批 + 清图批尾 + 区间行追加取号)
# reward_roll 发射+施加(格面资源掷定、摇号前权重合成层:
# bias_rewards 订阅命中合计=偏运权重加算(行 value_from magnitude 定影);零命中=原行集零扰动)
# reward_roll 发射归 BattleFxHost(偏运硬币 bias_rewards 订阅命中合计经注入查询口取回);
# 零命中/未接线=0=原行集零扰动
var luck_bias := 0
if _fx != null:
for h in _fx.dispatch(_ctx, "reward_roll", {"polarity": "positive", "cell_count": list.size()}):
if String(h.get("kind", "")) == "bias_rewards":
luck_bias += int(h.get("value", 0))
if luck_bias_query.is_valid():
luck_bias = luck_bias_query.call(list.size())
var cell_rows: Array[Variant] = _biased_cell_rows(luck_bias) # cell 池正向行 weight>0(偏运硬币装配时稀有正向 kinds 权重 +N——池结构零触碰)
var clear_rows: Array[Variant] = _pool_rows("clear_default")
# ① 身份批:每非封锁格恰一类型身份(雷安同池——同一行集同一权重分布)
@@ -365,4 +361,6 @@ func _diag(msg: String) -> void:
func _fatal(msg: String) -> void:
push_error("[地图战斗资源/fatal] " + msg)
if fatal_handler.is_valid():
fatal_handler.call(msg) # 组装层注入 orchestrator._fatal(封印传导,不绕第二中止通道)
assert(false, msg)
+9
View File
@@ -24,6 +24,9 @@ var _master_seed: int = -1 # -1 = 未播种(取数即致命,
var master_seed: int:
get: return _master_seed
var _counter: int = 0 # 全局取数计数器(唯一账本——快照契约 = 主种子 + 本计数器)
# 违约通道(未知用途标签/未播种取数两分支共用)——缺省空 Callable 退化 push_error+assert(现行为逐字保持);
# 注入后供负面套件断言「handler 被调用」(assert 只中断本函数,debug 下两态不可判别——记录位是唯一可观测面)。
var violation_handler: Callable = Callable()
# 事件日志(丙案新增):每冒险一会话,逐次取数落一行(scope/用途标签/派生种子)——不致命、失败静默降级
var _log_path := ""
var _log_fa: FileAccess = null
@@ -100,10 +103,16 @@ func pure_stream(seed_val: int, scope: int) -> RandomNumberGenerator:
func open_stream(purpose: String) -> RandomNumberGenerator:
if not PURPOSE_TAGS.has(purpose):
push_error("[RngService] 未知用途标签: %s" % purpose)
if violation_handler.is_valid():
violation_handler.call(purpose)
assert(false, "未知用途标签(标签词表封闭,EC12)")
return null # release 剥 assert 后经调用方空引用即刻崩=fail-fast 传导(合法路径不可达)
if _master_seed < 0:
push_error("[RngService] 未设 master_seed 即取数(EC7——冒险结构 未接线的显式暴露)")
if violation_handler.is_valid():
violation_handler.call(purpose)
assert(false, "未播种取数致命(EC7)")
return null # 同上(EC7 通道——release 下禁静默发号)
var scope: int = _counter
_counter = scope + 1
var rg := RandomNumberGenerator.new()
+7 -2
View File
@@ -173,6 +173,7 @@ func _replay_locked_click() -> void:
# 会话生命周期:_ready 建闲时态(永非 null——全组合根零空判责),
# 开局/续玩/调试两层各建新会话,旧会话整节点 queue_free()(帧末回收;新节点先挂树无真空窗)。
func _begin_session(adventure_session: bool) -> void:
_row_pacer.flush() # 会话更替即清队(待播行不住局态——机制保证,不靠时序约定)
if run_scope != null:
run_scope.queue_free()
run_scope = RunScope.new()
@@ -308,7 +309,7 @@ func _start_adventure_from_menu() -> void:
#(确认才删、取消只关窗——裁决 3)。"new_adventure" 消费方已随卡② 主菜单确认链摘除退役
#(mn6/V5——主菜单零确认残留),分派机制保留(卡④ 到位接入第三消费方不返工)。
func _on_confirm_accepted() -> void:
confirm_dialog.close()
_close_modal(confirm_dialog) # 模态栈单口
match _confirm_consumer:
"delete_slot":
save_manager.delete_slot(_confirm_consumer_slot)
@@ -317,7 +318,7 @@ func _on_confirm_accepted() -> void:
func _on_confirm_declined() -> void:
confirm_dialog.close() # 只关窗(取消无状态变更——裁决 3:取消不删)
_close_modal(confirm_dialog) # 只关窗(取消无状态变更——裁决 3:取消不删);关侧恢复归 ModalFlow
_confirm_consumer = ""
@@ -718,6 +719,8 @@ func _on_board_action(cell: Vector2i, kind: String) -> void:
if treasure_sel != 0 and kind == "dig":
_treasure_ui.attempt_use(treasure_sel, cell)
return
if run_scope.settlement == null:
return # 入口显式空防护(双保险——锁谓词正常态已挡,防锁判据异常时崩于空引用)
var outcomes: Array[Variant] = run_scope.settlement.minesweeper.settle_batch([{"action": kind, "cell": cell}])
var accepted: bool = not outcomes.is_empty() and bool(outcomes[0].accepted)
logger.log_action(kind, cell, accepted)
@@ -745,6 +748,8 @@ func _on_stop_pressed() -> void:
if _input_locked():
return
# stop 经批次提交(排雷玩法 D1×6①——不直调 行动经济)
if run_scope.settlement == null:
return # 入口显式空防护(同棋盘动作入口——零成本双保险)
var outcomes: Array[Variant] = run_scope.settlement.minesweeper.settle_batch([{"action": "stop"}])
logger.log_action("stop", Vector2i(-1, -1), not outcomes.is_empty() and bool(outcomes[0].accepted))
if not outcomes.is_empty() and String(outcomes[0].get("outcome", "")) == "voided":
+1 -7
View File
@@ -85,6 +85,7 @@ static func _on_save_select_load(root: Variant, slot: int) -> void:
static func _on_save_select_delete(root: Variant, slot: int) -> void:
root._confirm_consumer = "delete_slot"
root._confirm_consumer_slot = slot
root._open_modal(root.confirm_dialog) # 模态栈单口(显示编排/挂起兜底/遮挡记账——与其余覆盖层同口)
root.confirm_dialog.open("删除存档位 %d?删除后该档不可恢复。" % slot, "删除", "取消")
@@ -108,18 +109,11 @@ static func _sync_status_entry(root: Variant) -> void:
if root.status_panel == null or root._modals == null or root.board_zone == null or root.level_screen == null:
return
var map_on: bool = root.level_screen.visible
var battle_live: bool = root.run_scope.level_active or root.run_scope.settlement != null # 调试两层无 level_active——settlement 判据补位
root.board_zone.status_badge.visible = root.run_scope.level_active and not map_on
root.level_screen.status_badge.visible = map_on
# 置灰判据为排除式(调试浮窗开态不计入:对局中浮窗开着时角标仍可点)
root.board_zone.status_badge.disabled = root._modal_flow.badge_greyed_battle() # 互斥域谓词
root.level_screen.status_badge.disabled = root._modal_flow.badge_greyed_map()
# 状态角标判据(同上——重复语句在案):
# 战斗屏=对局内(冒险或调试两层)且地图屏未激活;地图屏=随屏;开始菜单零角标
root.board_zone.status_badge.visible = root.run_scope.level_active and not map_on
root.level_screen.status_badge.visible = map_on
root.board_zone.status_badge.disabled = root._modal_flow.badge_greyed_battle() # 互斥域谓词
root.level_screen.status_badge.disabled = root._modal_flow.badge_greyed_map()
# 调试浮层角标(全局浮层任何 UI 不遮挡——可见性不随场景变,只抬 offset
# 避让:地图屏底栏 85px 常驻,角标抬至底栏上方 16px;其余场景右下角 16px 贴底)
root.debug_badge.offset_bottom = -101.0 if map_on else -16.0
+2 -2
View File
@@ -37,8 +37,8 @@ func open(text: String, confirm_label: String, cancel_label: String = "取消")
_text.text = text
_confirm_btn.text = confirm_label
_cancel_btn.text = cancel_label
# z 序纪律:树序由组合根构建序保证——本弹窗在 main._build_ui 尾部
# add_child(开始层/过场层之后 = 子级最末,登记序 = 树序 = 绘制序恒最上)
# z 序纪律:层序唯一事实源=main.LAYER_CONTRACT(树序与登记序同源派生);开合单口
# 归组合根 ModalFlow(挂起兜底/遮挡记账/开侧同步)——本类只管文案与显示,不自行抬层
visible = true
+6
View File
@@ -253,12 +253,18 @@ func _ready() -> void:
elif not FileAccess.file_exists(csel_tpl % 2):
_problems.append("[savesel] sl7 确认前文件已被删(一次点击即删违契)")
else:
if not main._modal_flow.stack._occluded.has(main.confirm_dialog):
_problems.append("[savesel] sl7 确认窗未经模态栈开(直开旁路——wr2 单口失守)")
elif not main._input_locked():
_problems.append("[savesel] sl7 确认窗开时输入锁未生效(锁域失守——wr2)")
main.confirm_dialog.cancelled.emit() # 取消路径(真实信号——cancelled 钮同源)
await get_tree().process_frame
if main.confirm_dialog.is_open() or not FileAccess.file_exists(csel_tpl % 2):
_problems.append("[savesel] sl7 取消路径失守(应只关窗、文件仍在)")
elif main._confirm_consumer != "":
_problems.append("[savesel] sl8 取消后消费方状态量未清(分派面残留)")
if main._modal_flow.stack._occluded.has(main.confirm_dialog):
_problems.append("[savesel] sl7 取消后确认窗遮挡账未清(关侧未走模态栈——wr2)")
del_btn.pressed.emit() # 再开窗走确认路径
await get_tree().process_frame
main.confirm_dialog.confirmed.emit() # 确认路径(真实信号——confirmed 钮同源)
+1
View File
@@ -48,6 +48,7 @@ func _suites() -> Array:
# 脚本错误扫描对它属误伤——豁免只放行这一件,且仅限「错误扫描」一闸(退出码与断言下限照判)。
{"path": "res://tests/unit/data_config/reward_dangling_treasure_test.gd", "expect": 1, "negative": true}, # 秘宝双表校验
{"path": "res://tests/unit/data_config/counter_prefix_fatal_test.gd", "expect": 1, "negative": true}, # 卡② V9 前缀合法性+引用闭合加载致命
{"path": "res://tests/unit/core/run_time_fatal_test.gd", "expect": 1, "negative": true}, # 卡② wr5/wr6/wr7 运行期违约通道(fatal 封印传导/未知标签/未播种)
{"path": "res://tests/unit/data_config/rng_service_test.gd", "expect": 0},
{"path": "res://tests/unit/data_config/table_structure_guard_test.gd", "expect": 0}, # 表结构守卫宿主(adventure 配额与同调目录/enemy 带键行序/AC 单轨与掷点参数/variation 门控互锁/difficulty 修饰/reward 九型/meta 行族)
{"path": "res://tests/unit/data_config/rng_stream_boot_test.gd", "expect": 0}, # 启动初始化与统一 RNG 流宿主(data/tables 加载链/统一流黄金向量 m=42/derive_at 恒等/open_stream 取号序/new_adventure 计数器重置——批 B 自 smoke_all 迁入;源 :70 事件日志落盘条按裁决一转办集成宿主)
@@ -242,6 +242,25 @@ func _smoke_global_variant() -> void:
eco.board_skew = 50
var v50: Dictionary = _rows_weight_map(eco._biased_cell_rows(0))
Kit.check("域上界 v=50:守恒成立且逐行权重恒正(域 [0,50] 安全)", eco._pool_weight(eco._biased_cell_rows(0)) == 77 and int(v50.get("heal_minor", -1)) > 0)
# wr10 偏运判定收口+注入接线(p5 缝拆)——判定面夹具白盒装载(现网 game/data/tables/ 无产出
# bias_rewards 的数据行,先例=tree_rules_v11_test 读 _triggers);命中数前置写在消息里防空扫描面假绿。
var wrng = load("res://core/rng_service.gd").new()
wrng.set_master_seed(42)
var woc = load("res://core/battle_orchestrator.gd").new(_db, wrng)
var wfx = woc.fx_host()
var empty_bias: int = wfx.luck_bias_for(4)
wfx.engine._push_trigger("feat", "wr10_fixture", {"on": "reward_roll",
"do": {"kind": "bias_rewards", "value": 2}}, {})
var hit_rows: int = wfx.engine._triggers.size()
var loaded_bias: int = wfx.luck_bias_for(4)
Kit.check("wr10①判定面:空订阅=%d / 装载后命中 %d 行 → luck_bias_for(4)=%d(期望 0/1/2;命中 0 即判据空转)" % [empty_bias, hit_rows, loaded_bias],
empty_bias == 0 and hit_rows == 1 and loaded_bias == 2)
Kit.check("wr10②接线面:组装点注入的查询口有效且与宿主查询口同源",
woc._reward_economy.luck_bias_query.is_valid() and woc._reward_economy.luck_bias_query == wfx.luck_bias_for)
woc._reward_economy.board_skew = 0
var wbias: int = int(woc._reward_economy.luck_bias_query.call(4))
Kit.check("wr10③权重生效面:注入回调取回 %d → 偏运池合计 77+4*2(实测 %d)" % [wbias, woc._reward_economy._pool_weight(woc._reward_economy._biased_cell_rows(wbias))],
wbias == 2 and woc._reward_economy._pool_weight(woc._reward_economy._biased_cell_rows(wbias)) == 77 + 4 * 2)
# 普通图摇号全链路确定性(off 路径)——同种子普通图资源映射逐位相同(golden 对拍另有批前快照)
var nrng = load("res://core/rng_service.gd").new()
nrng.set_master_seed(42)
+125
View File
@@ -0,0 +1,125 @@
# 运行期违约通道负面套件(独立进程,期望退出码 1;回归态退出码 2)
# 卡② 三测试点:①wr5 协作件 _fatal 经组装点传导 orchestrator 封印(p5)+缺省形态不外溢;
# ②wr6 未知用途标签违约通道(p6);③wr7 未播种取数违约通道(p7)。
# 载体纪律(卡面 §7):注入只经编排器组装点——本文件不出现该注入成员的名称字样(防撤销证明②空转);
# 三测试点各用新建 RngService 实例(播种断言 _counter == 0);回归态判据只取可观测面——
# handler 记录位与封印是否落。return 位在 debug 下两态不可判别(assert 先中断函数本体),
# 走静态判据+代码评审注记,禁写成运行期断言。
# 违约路径必产 SCRIPT ERROR(assert 失败不可捕获)——故本件 manifest 登记 negative=true(test-conventions §5)。
# 运行:godot --headless --path game --script res://tests/unit/core/run_time_fatal_test.gd
extends SceneTree
const ConfigDBScript := preload("res://core/config_db.gd")
const RngScript := preload("res://core/rng_service.gd")
const BoardGeneratorScript := preload("res://core/board_generator.gd")
const RewardEconomyScript := preload("res://core/reward_economy.gd")
const OrchestratorScript := preload("res://core/battle_orchestrator.gd")
const TAG_OK := "board_gen" # 在册合法标签(PURPOSE_TAGS 含;wr7 必须用合法标签——未知标签检查排在播种检查之前)
func _init() -> void:
var db = ConfigDBScript.new()
if not db.load_tables("res://data/tables", true):
push_error("[run_time_fatal_test] 表加载失败——夹具不可用(回归)")
quit(2)
return
# 正向/缺省形态先跑完再触发违约(防 SCRIPT ERROR 混入正向判定)
if not _default_form(db):
quit(2)
return
if not _seal_propagation(db):
quit(2)
return
if not _unknown_tag_channel():
quit(2)
return
if not _unseeded_channel():
quit(2)
return
print("[run_time_fatal_test] 三点全绿:封印传导+未知标签通道+未播种通道〔卡② wr5/wr6/wr7〕")
quit(1)
# 缺省形态(契约 2):不接组装点的协作件违约只落自身 push_error+assert,不外溢封印
func _default_form(db) -> bool:
var rng = RngScript.new()
rng.set_master_seed(3)
var oc = OrchestratorScript.new(db, rng)
var bare_bg = BoardGeneratorScript.new()
bare_bg._fatal("缺省形态违约①(未接组装点——不应外溢封印)")
var bare_re = RewardEconomyScript.new(db, rng)
bare_re._fatal("缺省形态违约②(未接组装点——不应外溢封印)")
if oc.battle_result == "fatal_error":
push_error("[run_time_fatal_test/缺省] 未接组装点的违约外溢到编排器封印(缺省形态破)")
return false
# 合法路径对照(不注入 handler):播种后取在册标签照常发号
var rng_ok = RngScript.new()
rng_ok.set_master_seed(3)
var rg = rng_ok.open_stream(TAG_OK)
if rg == null or rng_ok.counter() != 1:
push_error("[run_time_fatal_test/缺省] 合法取号路径被改变(rg=%s counter=%d)" % [str(rg), rng_ok.counter()])
return false
return true
# ①wr5(p5):两协作件违约经组装点接线传导 → orchestrator.battle_result 落 "fatal_error"
func _seal_propagation(db) -> bool:
var rng_a = RngScript.new()
rng_a.set_master_seed(5)
var oc_bg = OrchestratorScript.new(db, rng_a)
oc_bg._board_generator._fatal("测试违约①棋盘生成")
if oc_bg.battle_result != "fatal_error":
push_error("[run_time_fatal_test/wr5] 棋盘生成违约未传导封印(battle_result=%r)" % oc_bg.battle_result)
return false
var rng_b = RngScript.new()
rng_b.set_master_seed(5)
var oc_re = OrchestratorScript.new(db, rng_b)
oc_re._reward_economy._fatal("测试违约②地图战斗资源")
if oc_re.battle_result != "fatal_error":
push_error("[run_time_fatal_test/wr5] 地图战斗资源违约未传导封印(battle_result=%r)" % oc_re.battle_result)
return false
return true
# ②wr6(p6):未知用途标签 → handler 记录位为真(判据主体)+拿不到可用 RNG;合法标签对照另行验过
func _unknown_tag_channel() -> bool:
var rng_bad = RngScript.new()
rng_bad.set_master_seed(7)
if not _inject_handler(rng_bad, "wr6"):
return false
var recorded: Array = []
rng_bad.violation_handler = func(p: String) -> void: recorded.append(p)
var rg = rng_bad.open_stream("bogus_purpose")
if recorded.size() != 1 or String(recorded[0]) != "bogus_purpose":
push_error("[run_time_fatal_test/wr6] 未知标签违约通道未记录(recorded=%s)" % str(recorded))
return false
if rg != null:
push_error("[run_time_fatal_test/wr6] 未知标签竟返回可用 RNG(违约被放行)")
return false
return true
# ③wr7(p7):未播种取数 → handler 记录位为真+拿不到可用 RNG;同标签播种对照另行验过
func _unseeded_channel() -> bool:
var rng_bad = RngScript.new() # 不播种(_master_seed 缺省 -1)
if not _inject_handler(rng_bad, "wr7"):
return false
var recorded: Array = []
rng_bad.violation_handler = func(p: String) -> void: recorded.append(p)
var rg = rng_bad.open_stream(TAG_OK)
if recorded.size() != 1 or String(recorded[0]) != TAG_OK:
push_error("[run_time_fatal_test/wr7] 未播种违约通道未记录(recorded=%s)" % str(recorded))
return false
if rg != null:
push_error("[run_time_fatal_test/wr7] 未播种竟返回可用 RNG(EC7 违约被放行)")
return false
return true
# 成员在位前提检查(成员被移除时走干净退出 2,不落「赋值不存在的属性」式中途中断)
func _inject_handler(rng, label: String) -> bool:
if not (rng.get("violation_handler") is Callable):
push_error("[run_time_fatal_test/%s] violation_handler 成员缺失(p6/p7 形态未实装——回归)" % label)
return false
return true
@@ -0,0 +1 @@
uid://cer3wlsqhjy7i
@@ -139,6 +139,14 @@ func _init() -> void:
"跨区弹道按行 target_id 解析站桩(id 与名同传——d12)〔wd12〕")
check(ffx.calls.size() == 2, "两路解析成功后演出层各接一拍(记参面非空)〔wd12〕")
rp.free()
# ---- 断言⑤:会话更替即清队(wr4——前置夹具入队再走真实入口 _begin_session;
# 无夹具则改前改后皆绿:flush 无队列可清时两种形态同值)----
UiKit.motion_scale = 1.0 # 常态入队(减动效快路同帧直落不入队——夹具须真落队列)
main._on_step_row({"type": "damage", "target": "hero", "value": 1})
main._on_step_row({"type": "heal", "target": "hero", "value": 2})
check(main._row_queue.size() == 2, "会话更替夹具已入队(前提:%d 行)〔wr4〕" % main._row_queue.size())
main._begin_session(false)
check(main._row_queue.is_empty(), "会话更替后待播队列清零(_begin_session 内机制 flush)〔wr4〕")
_finish()