refactor(迭代35卡②T2·二口+T3 S1/S2/S6/S8): 消费侧切源落定+断言面换代主体——① level_runner 图源换代(_generator 取代模板解析,_assign_generated_graph 读 level_gen_params 参数档产图并过 LevelTable.validate_graph,违规起关拒绝零兜底)+_entry_nodes 多入口首跳+_node_id 句柄单空间+enc node_depth 取节点 depth 值;② adventure_runner/main/save_manager 模板链路退役(first_script 参数与 template_pick 计算全删,断点 template_pick 键换生成图代差标记键 graph_gen_v2,缺键旧断点 resume 显式拒绝禁静默回放);③ level.tres 三行 templates 键清出(15 张模板数据出表,行级字段留,表尾注释改指参数档+生成器);④ sim/probe 随动(run_level/run_adventure 尾参退役、MC 出数字段 level_templates→level_maps 值=关卡#主种子、探针注释性质化、boss_depth 取节点 depth 键);⑤ 断言面换代:node_map_templates 宿主整段重写为生成图确定性套(纯函数复现/换种子变图/零计数器号位依赖/单一图源/种子 42 两开局同图/断点重生成同图/缺代差键拒绝/表无 templates/收束置位三条保留),breakpoint_resume 新增 P7 六断言(快照无已抽模板键、代差标记、停点句柄与已到序往返、续玩同图、旧断点拒绝、同种子重开局同图),S8 单测随动 first_script 键清退+断点夹具换代+meta 参数面 extra 键样换 treasure_unlocked(语义保持),S2 生成图取数面三宿主替换(level_active_nodes 走参数档+种子 42,消费点深度键换 depth,round_flow 死件 _boss_depth 随删),adventure_feats 覆盖义务段改真实解析链判据(敌侧落带≥请求深度+板侧命中+非空扫描面)、奖励节点段换「图侧含件+真实选路点选呈停兑付」双断言(旧单链必经形状前提随模板消亡,非放宽:经停即呈候选由真实 pick_next_node 入口证明)、商店余额段按每关恰一店投影改三关读数
This commit is contained in:
1 parent
6566200220
commit
9ee3bea194
22 files changed
+460
-362
No files matched your search
@@ -290,7 +290,7 @@ func _smoke_encounter_roll() -> void:
|
||||
# 战斗结算 roll_lo 下限 + 敌人系统 spawn count<1 fail-fast 双闸;单深度单种子是盲区,全域扫描才钉死不变式)
|
||||
var depths: Array = []
|
||||
for node in _level_battle_nodes(1):
|
||||
depths.append(int(node.get("node_depth", -1)))
|
||||
depths.append(int(node.get("depth", -1)))
|
||||
var scan_bad := 0
|
||||
var scan_total := 0
|
||||
for i in range(12):
|
||||
@@ -317,22 +317,11 @@ func _smoke_encounter_roll() -> void:
|
||||
# 节点类型 → 敌人表 row_kind(非 Boss 战斗位恒 battle——深度带批寻址第三维)
|
||||
func _row_kind_of(level_index: int, depth: int) -> String:
|
||||
for n in _level_active_nodes(level_index):
|
||||
if int(n.get("node_depth", -1)) == depth and String(n.get("type", "")) == "boss":
|
||||
if int(n.get("depth", -1)) == depth and String(n.get("type", "")) == "boss":
|
||||
return "boss"
|
||||
return "battle"
|
||||
|
||||
|
||||
# 该关 Boss 节点深度(单终点图的终点编号——数据驱动,禁写死坐标)
|
||||
|
||||
|
||||
# 该关 Boss 节点深度(单终点图的终点编号——数据驱动,禁写死坐标)
|
||||
func _boss_depth(level_index: int) -> int:
|
||||
for n in _level_active_nodes(level_index):
|
||||
if String(n.get("type", "")) == "boss":
|
||||
return int(n.get("node_depth", -1))
|
||||
return -1
|
||||
|
||||
|
||||
# 首遇带数据驱动定位(契约 8):该 (L, battle, enemy_id) 的最低 depth_band,禁写死坐标数字
|
||||
|
||||
|
||||
@@ -353,15 +342,12 @@ func _first_encounter_band(level_index: int, enemy_id: String) -> int:
|
||||
# 模板感知取节点表(row.templates 非空取模板 0=现行固定图,保持既有断言语义)
|
||||
|
||||
|
||||
# 模板感知取节点表(row.templates 非空取模板 0=现行固定图,保持既有断言语义)
|
||||
# 生成图取数面(图 = f(关卡行参数档, 种子 42)——与 runner 起关同图;节点键 id/type/depth/next)
|
||||
func _level_active_nodes(level_index: int) -> Array:
|
||||
for row in _db.level.rows:
|
||||
if int(row.level_index) == level_index:
|
||||
var tpls: Array = row.get("templates", [])
|
||||
if not tpls.is_empty():
|
||||
return tpls[0].get("nodes", [])
|
||||
return row.get("nodes", [])
|
||||
return []
|
||||
var rng = load("res://core/rng_service.gd").new()
|
||||
rng.set_master_seed(42)
|
||||
var gen = NodeMapGenerator.new(rng)
|
||||
return (gen.generate(_db.level_gen_params_row(level_index), 42) as Dictionary).get("nodes", [])
|
||||
|
||||
|
||||
func _level_battle_nodes(level_index: int) -> Array:
|
||||
|
||||
@@ -58,7 +58,7 @@ func _smoke_adventure_api() -> void:
|
||||
adr.level_started.connect(_api_mark_started.bind(sig_seq))
|
||||
adr.choices_presented.connect(_api_mark_choices.bind(sig_seq, obs))
|
||||
adr.node_entered.connect(_api_mark_node.bind(sig_seq))
|
||||
Kit.check("冒险启动受理(主种子注入恰一次——state=running;first_script=固定模板 0——精确路径断言需要现行图,零抽签)", adr.start_adventure(42, {"first_script": true}))
|
||||
Kit.check("冒险启动受理(主种子注入恰一次——state=running;种子 42=首局固定种子生成图——精确路径断言需要确定图(卡② 代差))", adr.start_adventure(42))
|
||||
Kit.check("进行中重复 start_adventure 拒绝(重开须待结算完毕)", not adr.start_adventure(43))
|
||||
Kit.check("停点前移次序契约:level_started 先于入口候选呈现(反序选路口会被呈现层 runner 空防御丢弃)", sig_seq.size() == 2 and String(sig_seq[0]) == "level_started" and String(sig_seq[1]) == "choices_presented")
|
||||
Kit.check("入口停点:选路态 + 入口单候选 + 首战未开(呈现零 RNG 消耗——pick 前零战斗域)",
|
||||
@@ -108,26 +108,27 @@ func _api_mark_node(_node: Dictionary, _info: Dictionary, seq: Array) -> void:
|
||||
func _smoke_adventure() -> void:
|
||||
print("--- 冒险结构验收(三关序列 · 大奖励 · 摘要五件事 · 账本封闭)---")
|
||||
var autoplay = load("res://sim/level_autoplay.gd")
|
||||
# ① 表与覆盖义务(序列关数 = 行集关数;行集内每个战斗/Boss 节点跨表精确行集——覆盖义务断言;
|
||||
# enemy 侧键域 = (level_index, row_kind, depth_band)——战斗节点查 battle 键域、Boss 节点查 boss 键域,
|
||||
# 同带多行合法,带键聚合天然兼容〔fv24〕)
|
||||
# ① 表与覆盖义务(序列关数 = 行集关数;节点源 = 参数档在固定种子下的生成图,
|
||||
# 每个战斗/Boss 节点经 ConfigDB 真实解析链查敌行集与棋盘行——顶 clamp 消解禁〔fv24〕)
|
||||
Kit.check("序列关数 = 行集关数(三关正式序列生效)", int(_db.level.rows.size()) == 3)
|
||||
var bg_keys := {}
|
||||
for row in _db.board_gen.rows:
|
||||
bg_keys["%d_%d" % [int(row.level_index), int(row.node_depth)]] = true
|
||||
var en_keys := {}
|
||||
for row in _db.enemy.rows:
|
||||
en_keys["%d_%s_%d" % [int(row.level_index), String(row.get("row_kind", "")), int(row.get("depth_band", -1))]] = true
|
||||
var miss := 0
|
||||
for row in _db.level.rows:
|
||||
for n in _level_active_nodes(int(row.level_index)):
|
||||
var cov_miss := 0
|
||||
var cov_scan := 0
|
||||
for li in range(1, int(_db.level.rows.size()) + 1):
|
||||
for n in _level_active_nodes(li):
|
||||
var nt := String(n.get("type", ""))
|
||||
if nt != "battle" and nt != "boss":
|
||||
continue
|
||||
var key := "%d_%s_%d" % [int(row.level_index), nt, int(n.get("node_depth", -1))]
|
||||
if not bg_keys.has("%d_%d" % [int(row.level_index), int(n.get("node_depth", -1))]) or not en_keys.has(key):
|
||||
miss += 1
|
||||
Kit.check("行集内每个战斗/Boss 节点在 board_gen + enemy 有精确行集(缺失 %d——带键 (L,row_kind,depth_band))" % miss, miss == 0)
|
||||
var d: int = int(n.get("depth", -1))
|
||||
cov_scan += 1
|
||||
var rk := "boss" if nt == "boss" else "battle"
|
||||
var rs: Array[Dictionary] = _db.resolve_enemy_rowset(li, d, rk)
|
||||
if rs.is_empty() or int(rs[0].get("depth_band", -1)) < d:
|
||||
cov_miss += 1
|
||||
if not (_db.resolve_board_gen(li, d) as Dictionary).is_empty():
|
||||
continue
|
||||
cov_miss += 1
|
||||
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)
|
||||
var sum: Dictionary = st["summary"]
|
||||
@@ -151,7 +152,65 @@ func _smoke_adventure() -> void:
|
||||
elif (ev as Dictionary).has("curio_resolved"):
|
||||
resolves.append((ev as Dictionary)["curio_resolved"])
|
||||
Kit.check("通关三选一恰呈现 2 次、恰兑付 2 次(L1/L2 后——末关不发;resolves 含节点回执另计)", picks.size() == 2 and resolves.size() == 2 + node_picks)
|
||||
Kit.check("奖励节点三选一 ≥1 次(L1 链必经 1 个;L2/L3 图可选路经若干——实际经停 %d)" % node_picks, node_picks >= 1)
|
||||
# 奖励节点在生成图里是可选路位(旧单链模板的「L1 必经 1 个」形状前提随模板消亡)——
|
||||
# 接线证明拆两半:图侧每关含 ≥1 奖励节点,行进侧种子扫描内确有经停(扫描零经停=三选一接线不可达)
|
||||
var reward_levels := 0
|
||||
for li in range(1, int(_db.level.rows.size()) + 1):
|
||||
var rc := 0
|
||||
for n in _level_active_nodes(li):
|
||||
if String(n.get("type", "")) == "reward":
|
||||
rc += 1
|
||||
if rc >= 1:
|
||||
reward_levels += 1
|
||||
Kit.check("三关生成图各含 ≥1 奖励节点(节点三选一候选位在图)", reward_levels == 3)
|
||||
# 行进侧接线证明走真实选路入口:满配打赢入口战→岔口点选奖励分支→三选一候选可取且停点在途
|
||||
# (确定性评分策略恒选战斗/恢复分支,奖励位不在扫描路由上——旧「链必经」形状断言的替代形态)
|
||||
var adr_rw = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(42))
|
||||
adr_rw.set_hero_override(900, 400)
|
||||
Kit.check("奖励分支场景冒险启动(seed 42 满配)", adr_rw.start_adventure(42))
|
||||
var r_rw = adr_rw.runner()
|
||||
var rw_st := _adv_st()
|
||||
var rw_cands: Array = []
|
||||
var rw_guard := 30
|
||||
while r_rw != null and rw_guard > 0:
|
||||
rw_guard -= 1
|
||||
if r_rw.awaiting_choice():
|
||||
var rw_id := -1
|
||||
for c in r_rw.next_candidates():
|
||||
if String((c as Dictionary).get("type", "")) == "reward":
|
||||
rw_id = int((c as Dictionary).get("id", -1))
|
||||
break
|
||||
if rw_id >= 0:
|
||||
r_rw.pick_next_node(rw_id)
|
||||
rw_cands = (adr_rw.node_curio_candidates() as Dictionary).get("candidates", []) as Array
|
||||
break
|
||||
var rw_ids: Array = r_rw.next_candidate_ids()
|
||||
if rw_ids.is_empty():
|
||||
break
|
||||
r_rw.pick_next_node(int(rw_ids[0]))
|
||||
continue
|
||||
var b_rw = r_rw.current_battle()
|
||||
if b_rw == null:
|
||||
break
|
||||
autoplay._autoplay_battle(r_rw, b_rw, rw_st, null)
|
||||
var rw_pending_ok := false
|
||||
if not rw_cands.is_empty():
|
||||
adr_rw.present_node_curio(rw_cands)
|
||||
rw_pending_ok = not (adr_rw._node_curio_pending as Dictionary).is_empty()
|
||||
var rw_pc: Dictionary = {}
|
||||
for c in rw_cands:
|
||||
var cd := c as Dictionary
|
||||
if String(cd.get("kind", "")) != "feat_ticket":
|
||||
rw_pc = cd
|
||||
break
|
||||
if rw_pc.is_empty() and not rw_cands.is_empty():
|
||||
rw_pc = rw_cands[0] as Dictionary
|
||||
var rw_pick_ok := false
|
||||
if rw_pending_ok and not rw_pc.is_empty():
|
||||
rw_pick_ok = adr_rw.apply_node_curio(String(rw_pc.get("kind", "")),
|
||||
String(rw_pc.get("tier", "")), String(rw_pc.get("feat_id", "")))
|
||||
Kit.check("奖励分支点选→三选一停点在途且兑付受理(候选 %d 件)" % rw_cands.size(),
|
||||
rw_pending_ok and rw_pick_ok)
|
||||
var node_resolves := 0
|
||||
for r0 in resolves:
|
||||
if String((r0 as Dictionary).get("context", "")) == "reward_node":
|
||||
@@ -260,7 +319,7 @@ func _smoke_adventure() -> void:
|
||||
# ⑥ 重复宣布拒绝(settle 后再调 _on_level_finished——摘要零变化)
|
||||
var adr6 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(7))
|
||||
adr6.set_hero_override(900, 400)
|
||||
Kit.check("重复拒绝场景:冒险启动(seed 7——固定模板 0 同上)", adr6.start_adventure(7, {"first_script": true}))
|
||||
Kit.check("重复拒绝场景:冒险启动(seed 7——生成图确定同上)", adr6.start_adventure(7))
|
||||
adr6.runner().pick_next_node(int(adr6.runner().next_candidate_ids()[0])) # 入口停点 pick 进首战
|
||||
var b6 = adr6.runner().current_battle()
|
||||
var st6 := _adv_st()
|
||||
@@ -284,13 +343,13 @@ func _smoke_adventure() -> void:
|
||||
Kit.check("每战完成入账恰一次(gain %d = battles %d——GP 袋件入账单列)" % [gains, int(st["battles"])], gains == int(st["battles"]))
|
||||
Kit.check("钱包 clear 恰一次且为末事件(关卡收束不清——GP 冒险收束清零归冒险结构)", clears == 1 and wallet.size() > 0 and String(wallet[wallet.size() - 1]["event"]) == "clear")
|
||||
var shop_seq: Array = st["shop_gp_seq"]
|
||||
Kit.check("L2 商店房到达余额 > 0(%d——L1 积累跨关延续;两关商店房必经各 1)" % (shop_seq[0] if shop_seq.size() > 0 else -1), shop_seq.size() == 2 and int(shop_seq[0]) > 0)
|
||||
Kit.check("三关商店房各到达一次(每关入口→终点路径恰穿一店的驱动投影)且首达余额 > 0(%d——GP 跨关延续)" % (shop_seq[0] if shop_seq.size() > 0 else -1), shop_seq.size() == 3 and int(shop_seq[0]) > 0)
|
||||
# ⑧ 加算区同账(手工场景:L2 商店购买 → 三选一直调——同一本账累加 + 部分回血封顶)
|
||||
var same := {}
|
||||
for s in range(42, 92):
|
||||
var a8 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(s))
|
||||
a8.set_hero_override(900, 400)
|
||||
if not a8.start_adventure(s, {"first_script": true}): # 固定模板 0——种子扫描语义按现行图
|
||||
if not a8.start_adventure(s): # 种子扫描语义按生成图(同种子同图)
|
||||
continue
|
||||
if _adv_drive_to_shop(a8, _adv_st(), autoplay):
|
||||
same = {"a": a8, "s": s}
|
||||
@@ -335,7 +394,7 @@ func _smoke_adventure() -> void:
|
||||
# ⑩ 放弃(战斗中拒绝 · 非战斗期受理同失败型结算)
|
||||
var a10 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(15))
|
||||
a10.set_hero_override(900, 400)
|
||||
Kit.check("放弃场景:冒险启动(seed 15——固定模板 0 同上)", a10.start_adventure(15, {"first_script": true}))
|
||||
Kit.check("放弃场景:冒险启动(seed 15——生成图确定同上)", a10.start_adventure(15))
|
||||
Kit.check("放弃场景:入口停点 pick 进首战(停点前移批 2026-09-23 驱动口径)", a10.runner().pick_next_node(int(a10.runner().next_candidate_ids()[0])))
|
||||
Kit.check("战斗中 abandon 拒绝(战斗域单事务在飞)", not a10.abandon())
|
||||
var b10 = a10.runner().current_battle()
|
||||
@@ -348,7 +407,7 @@ func _smoke_adventure() -> void:
|
||||
# ⑪ 池耗即枯竭 + 池收缩 + 满血回 0(TP0——white-box:耗尽 attack/ap 令牌后候选零出现;
|
||||
# 无种类次数上限,「达上限剔除」语义由牌库耗尽承载)
|
||||
var a11 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(11))
|
||||
Kit.check("剔牌库/替换场景:冒险启动(seed 11——道具解锁集只含 wide_scout_band)", a11.start_adventure(11, {"first_script": true, "treasure_unlocked": {"wide_scout_band": true}}))
|
||||
Kit.check("剔牌库/替换场景:冒险启动(seed 11——道具解锁集只含 wide_scout_band)", a11.start_adventure(11, {"treasure_unlocked": {"wide_scout_band": true}}))
|
||||
for di in range(a11._curio_deck.size() - 1, -1, -1):
|
||||
var dk := String((a11._curio_deck[di] as Dictionary).get("kind", ""))
|
||||
if dk == "attack" or dk == "ap":
|
||||
@@ -370,7 +429,7 @@ func _smoke_adventure() -> void:
|
||||
Kit.check("池收缩——40 次重抽零 aux 候选(停点渠道 aux 卡退役,迭代 11 第①批 T7)", not treasure_seen)
|
||||
# 满血回 0 合法(通关回血封顶口径)
|
||||
var a12 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(12))
|
||||
a12.start_adventure(12, {"first_script": true})
|
||||
a12.start_adventure(12)
|
||||
var cap := {"payload": {}}
|
||||
a12.curio_choice_presented.connect(func(p): cap["payload"] = p)
|
||||
var max_full := int(_db.hero.max_hp) + int(a12._base_override.get("max_hp", 0))
|
||||
@@ -379,7 +438,7 @@ func _smoke_adventure() -> void:
|
||||
Kit.check("满血通关回血 0 合法(封顶口径——heal_gained=0 非负溢出)", int(cap["payload"].get("heal_gained", -1)) == 0)
|
||||
# ⑬ 奖励节点共享池(同库同池同摘牌;TP0:attack 令牌耗尽后节点域同样零出现)
|
||||
var a13 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(13))
|
||||
Kit.check("节点共享池场景:冒险启动(seed 13)", a13.start_adventure(13, {"first_script": true, "treasure_unlocked": {"wide_scout_band": true}}))
|
||||
Kit.check("节点共享池场景:冒险启动(seed 13)", a13.start_adventure(13, {"treasure_unlocked": {"wide_scout_band": true}}))
|
||||
for di13 in range(a13._curio_deck.size() - 1, -1, -1):
|
||||
if String((a13._curio_deck[di13] as Dictionary).get("kind", "")) == "attack":
|
||||
a13._curio_deck.remove_at(di13)
|
||||
@@ -411,7 +470,7 @@ func _smoke_feats() -> void:
|
||||
Kit.check("feat_table 已加载(30 件=基础 27+迁入 2+卡② 完美保险 1——迭代 31 卡② 换代)", _db.feats != null and (_db.feats.rows as Array).size() == 30)
|
||||
Kit.check("feat_table.validate 零违例(四级 rarity/cap 域/trigger 词表/行键白名单全过)", _db.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, {"first_script": true}))
|
||||
Kit.check("专长带场景:冒险启动(seed 71)", a_r0.start_adventure(71))
|
||||
var feat_tokens := 0
|
||||
var tier_counts := {}
|
||||
for t in a_r0._curio_deck:
|
||||
@@ -465,7 +524,7 @@ func _smoke_feats() -> void:
|
||||
var live_rows: FeatTable = _db.feats
|
||||
_db.feats = rt
|
||||
var a_r = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(72))
|
||||
Kit.check("专长带场景:冒险启动(seed 72——注入 5 行临时表)", a_r.start_adventure(72, {"first_script": true}))
|
||||
Kit.check("专长带场景:冒险启动(seed 72——注入 5 行临时表)", a_r.start_adventure(72))
|
||||
var deck_relic := 0
|
||||
for t in a_r._curio_deck:
|
||||
if String((t as Dictionary).get("kind", "")) == "relic":
|
||||
@@ -485,7 +544,7 @@ func _smoke_feats() -> void:
|
||||
Kit.check("节点渠道零专长(60 次重抽零 relic——专长在非 Boss 渠道只以专长凭证形态出现)", not node_relic_seen)
|
||||
# T5 专长凭证行为断言(A5 拿到即开 / A6 保底计数——契约④四口径;独立 runner 防污染 a_r 牌库)
|
||||
var a_t = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(91))
|
||||
a_t.start_adventure(91, {"first_script": true})
|
||||
a_t.start_adventure(91)
|
||||
var stock_before := {}
|
||||
for kk in ["attack", "gp", "heal"]:
|
||||
stock_before[kk] = a_t.pool_stock(kk)
|
||||
@@ -809,12 +868,7 @@ func _fresh_rng(seed_val: int):
|
||||
# ---- ① GP 冒险钱包·到账 ----
|
||||
|
||||
|
||||
# 模板感知取节点表(row.templates 非空取模板 0=现行固定图,保持既有断言语义)
|
||||
# 生成图取数面(图 = f(关卡行参数档, 种子 42)——与 runner 起关同图;节点键 id/type/depth/next)
|
||||
func _level_active_nodes(level_index: int) -> Array:
|
||||
for row in _db.level.rows:
|
||||
if int(row.level_index) == level_index:
|
||||
var tpls: Array = row.get("templates", [])
|
||||
if not tpls.is_empty():
|
||||
return tpls[0].get("nodes", [])
|
||||
return row.get("nodes", [])
|
||||
return []
|
||||
var gen = NodeMapGenerator.new(_fresh_rng(42))
|
||||
return (gen.generate(_db.level_gen_params_row(level_index), 42) as Dictionary).get("nodes", [])
|
||||
@@ -147,8 +147,52 @@ func _init() -> void:
|
||||
Kit.check_quiet(adr_i.runner().pick_next_node(int(adr_i.runner().next_candidate_ids()[0])), "P6 续玩选路进首战")
|
||||
Kit.check_quiet(adr_i.runner().current_battle().attune_has("morning_order"), "P6 回局首战装配集仍含 morning_order(停点往返载体态一致)")
|
||||
|
||||
# ---- Part 7:生成图换代断言面(快照键换代、停点句柄往返、续玩按种子+参数档重生成同图)----
|
||||
var rng_j = load("res://core/rng_service.gd").new()
|
||||
var sm_j = SaveManagerScript.new(_bp_path)
|
||||
var adr_j = AdventureRunnerScript.new(db, rng_j)
|
||||
adr_j.set_save_manager(sm_j)
|
||||
Kit.check_quiet(adr_j.start_adventure(910042, params), "P7 冒险启动(seed 910042——生成图开局)")
|
||||
var runner_j = adr_j.runner()
|
||||
var graph_j: Array = runner_j.nodes()
|
||||
var bp_j: Dictionary = sm_j.breakpoint_snapshot()
|
||||
Kit.check_quiet(not bp_j.has("template_pick"),
|
||||
"P7 断点快照无已抽模板键(模板链路退役——件核面在档)")
|
||||
Kit.check_quiet(int(bp_j.get("graph_gen_v2", 0)) == 1,
|
||||
"P7 断点含生成图代差标记键(换代承载)")
|
||||
var entry_id: int = int(runner_j.next_candidate_ids()[0])
|
||||
var mid_id: int = _first_out_id(graph_j, entry_id)
|
||||
var bp_mid: Dictionary = bp_j.duplicate(true)
|
||||
bp_mid["node_id"] = mid_id
|
||||
bp_mid["visited_ids"] = [entry_id]
|
||||
var adr_m = AdventureRunnerScript.new(db, load("res://core/rng_service.gd").new())
|
||||
Kit.check_quiet(adr_m.resume_adventure(bp_mid, params), "P7 中途停点断点续玩受理(句柄落档形态)")
|
||||
var runner_m = adr_m.runner()
|
||||
Kit.check_quiet(int(runner_m.current_node_id()) == mid_id,
|
||||
"P7 停点句柄回读一致(图内 id 单空间寻址)")
|
||||
Kit.check_quiet(runner_m.visited_ids() == [entry_id], "P7 已到节点序按句柄往返一致")
|
||||
Kit.check_quiet(runner_m.nodes() == graph_j, "P7 续玩重生成图与中断前逐位同图(断点不存图快照)")
|
||||
var adr_l = AdventureRunnerScript.new(db, load("res://core/rng_service.gd").new())
|
||||
var bp_legacy: Dictionary = bp_mid.duplicate(true)
|
||||
bp_legacy.erase("graph_gen_v2")
|
||||
Kit.check_quiet(adr_l.resume_adventure(bp_legacy, params) == false,
|
||||
"P7 缺生成图代差标记键的旧断点显式拒绝(禁按新句柄空间静默回放)")
|
||||
var adr_n = AdventureRunnerScript.new(db, load("res://core/rng_service.gd").new())
|
||||
Kit.check_quiet(adr_n.start_adventure(910042, params) and adr_n.runner().nodes() == graph_j,
|
||||
"P7 同种子重开局同图(开局入口复现承载)")
|
||||
|
||||
_clear_disk()
|
||||
quit(Kit.finish("breakpoint_resume_test", "PASS — 断点往返/续玩确定性/幂等/退出清断点/装配存档回归 全绿"))
|
||||
quit(Kit.finish("breakpoint_resume_test", "PASS — 断点往返/续玩确定性/幂等/退出清断点/装配存档回归/生成图换代 全绿"))
|
||||
|
||||
|
||||
# 句柄寻址夹具辅助:取生成图中指定句柄节点的首个出边目标(构造中途停点落档形态)
|
||||
func _first_out_id(nodes: Array, node_id: int) -> int:
|
||||
for node in nodes:
|
||||
var n := node as Dictionary
|
||||
if int(n.get("id", -1)) == node_id:
|
||||
var outs: Array = n.get("next", [])
|
||||
return int(outs[0]) if not outs.is_empty() else -1
|
||||
return -1
|
||||
|
||||
|
||||
func _board_fingerprint(b) -> String:
|
||||
|
||||
@@ -53,12 +53,12 @@ func _init() -> void:
|
||||
Kit.check_quiet(lr.spend_gp(20) == true and int(lr.gp) == 10, "足额支出扣账(30→10)")
|
||||
|
||||
# ---- ⑥ Y 断点随存(缺陷锚:续玩自参数面整额重注入=用完 Y 退菜单再续玩满血复活)----
|
||||
var bp_y := {"level_index": 1, "hero_hp": 10, "attune_refresh_y": 2}
|
||||
var bp_y := {"level_index": 1, "hero_hp": 10, "attune_refresh_y": 2, "graph_gen_v2": 1}
|
||||
var adr_r = load("res://core/adventure_runner.gd").new(db, load("res://core/rng_service.gd").new())
|
||||
Kit.check_quiet(adr_r.resume_adventure(bp_y, {"hero_override": {}, "attune_refresh_y": 3}), "续玩受理(最小断点+参数面)")
|
||||
Kit.check_quiet(adr_r._attune_refresh_y == 2, "断点有键=Y 余量随断点还原(花 1 剩 2——不再整额重注入=满血复活堵死)")
|
||||
var adr_o = load("res://core/adventure_runner.gd").new(db, load("res://core/rng_service.gd").new())
|
||||
Kit.check_quiet(adr_o.resume_adventure({"level_index": 1, "hero_hp": 10}, {"hero_override": {}, "attune_refresh_y": 3}), "旧断点续玩受理(快照缺键)")
|
||||
Kit.check_quiet(adr_o.resume_adventure({"level_index": 1, "hero_hp": 10, "graph_gen_v2": 1}, {"hero_override": {}, "attune_refresh_y": 3}), "旧断点续玩受理(快照缺键)")
|
||||
Kit.check_quiet(adr_o._attune_refresh_y == 3, "旧断点缺键=回退参数面整额注入(旧档行为零变化)")
|
||||
|
||||
quit(Kit.finish("curio_refresh_paid"))
|
||||
|
||||
@@ -22,7 +22,7 @@ func _init() -> void:
|
||||
quit(1)
|
||||
return
|
||||
var a = load("res://core/adventure_runner.gd").new(db, load("res://core/rng_service.gd").new())
|
||||
if not a.start_adventure(301, {"first_script": true}):
|
||||
if not a.start_adventure(301):
|
||||
Kit.check_quiet(false, "V9 前置:冒险启动")
|
||||
quit(Kit.finish("feat_vouchers"))
|
||||
return
|
||||
|
||||
@@ -29,61 +29,71 @@ func _init() -> void:
|
||||
|
||||
# ---- 夹具 ----
|
||||
|
||||
# 图形态夹具(id = node_depth):单干 0→1→2→3→4→5→6→7 加一条 6→3 跨层跳边。
|
||||
# BFS 层:0=0/1=1/2=1/3=2/4=3/5=4/6=5/7=6——锚 6 时候选 3 层距 3 = N+1。
|
||||
# 节点构造器(生成图口径:id=图内唯一句柄、depth=自入口最长路径步数、next=出边句柄数组)
|
||||
static func _gn(id: int, depth: int, type_: String, next_ids: Array) -> Dictionary:
|
||||
return {"id": id, "type": type_, "depth": depth, "next": next_ids}
|
||||
|
||||
|
||||
# 图形态夹具(id = 图内句柄,depth = 自入口沿边严格前进的深度):干 0→1→{2,8},2→3,
|
||||
# 8→4→5→6,长跳边 6→3(与 2→3 同落节点 3,目标 depth 严格大于源——生成图连线铁律),
|
||||
# 3 经尾链 9→10→11 汇入终点 7(7 的 depth = 自入口最长路径步数 10)。
|
||||
# BFS 层:0=0/1=1/2=1/3=2/4=3/5=4/6=5/7=6(8=2/9=3/10=4/11=5)——锚 6 时候选 3 层距 3 = N+1。
|
||||
func _graph_nodes() -> Array:
|
||||
return [
|
||||
{"node_depth": 0, "type": "battle", "next": [1, 2]},
|
||||
{"node_depth": 1, "type": "battle", "next": [2]},
|
||||
{"node_depth": 2, "type": "reward", "next": [3]},
|
||||
{"node_depth": 3, "type": "rest", "next": [4]},
|
||||
{"node_depth": 4, "type": "battle", "next": [5]},
|
||||
{"node_depth": 5, "type": "battle", "next": [6]},
|
||||
{"node_depth": 6, "type": "battle", "next": [7, 3]},
|
||||
{"node_depth": 7, "type": "boss", "next": []},
|
||||
_gn(0, 0, "battle", [1, 2]),
|
||||
_gn(1, 1, "battle", [2, 8]),
|
||||
_gn(2, 2, "reward", [3]),
|
||||
_gn(3, 6, "rest", [9]),
|
||||
_gn(4, 3, "battle", [5]),
|
||||
_gn(5, 4, "battle", [6]),
|
||||
_gn(6, 5, "battle", [7, 3]),
|
||||
_gn(7, 10, "boss", []),
|
||||
_gn(8, 2, "battle", [4]),
|
||||
_gn(9, 7, "battle", [10]),
|
||||
_gn(10, 8, "battle", [11]),
|
||||
_gn(11, 9, "battle", [7]),
|
||||
]
|
||||
|
||||
|
||||
# 单链夹具(无 next 键,仿 L1 现役模板形态):编号空间 = 数组下标,node_depth 只出现在战斗/Boss 上
|
||||
# 单链退化形态 = 出度 ≤1 的图(显式 next 出边、编号 = id 句柄——无数组下标隐式边)
|
||||
func _chain_nodes() -> Array:
|
||||
return [
|
||||
{"node_depth": 0, "type": "battle"},
|
||||
{"node_depth": 1, "type": "battle"},
|
||||
{"node_depth": 2, "type": "battle"},
|
||||
{"node_depth": 3, "type": "battle"},
|
||||
{"type": "reward"},
|
||||
{"type": "rest"},
|
||||
{"node_depth": 4, "type": "boss"},
|
||||
_gn(0, 0, "battle", [1]),
|
||||
_gn(1, 1, "battle", [2]),
|
||||
_gn(2, 2, "battle", [3]),
|
||||
_gn(3, 3, "battle", [4]),
|
||||
_gn(4, 4, "reward", [5]),
|
||||
_gn(5, 5, "rest", [6]),
|
||||
_gn(6, 6, "boss", []),
|
||||
]
|
||||
|
||||
|
||||
# 表行夹具的正样本(无跨层跳边的合法图——表加载期只核行级字段,图合法性判据见 validate_graph·卡② T1 换代)
|
||||
func _valid_graph_nodes() -> Array:
|
||||
return [
|
||||
{"node_depth": 0, "type": "battle", "next": [1, 2]},
|
||||
{"node_depth": 1, "type": "battle", "next": [3]},
|
||||
{"node_depth": 2, "type": "reward", "next": [3]},
|
||||
{"node_depth": 3, "type": "battle", "next": [4]},
|
||||
{"node_depth": 4, "type": "boss", "next": []},
|
||||
_gn(0, 0, "battle", [1, 2]),
|
||||
_gn(1, 1, "battle", [3]),
|
||||
_gn(2, 1, "reward", [3]),
|
||||
_gn(3, 2, "battle", [4]),
|
||||
_gn(4, 3, "boss", []),
|
||||
]
|
||||
|
||||
|
||||
# 白盒 runner:夹具行集注入 db.level + level_row,停点态直设(anchor_id < 0 = 入口停点)
|
||||
func _runner(db, nodes: Array, anchor_id: int, candidates: Array,
|
||||
graph_mode: bool = true, n: int = 2):
|
||||
# 白盒 runner:夹具行集注入 db.level + level_row,停点态直设(anchor_id < 0 = 入口停点;
|
||||
# 锚点与候选的编号空间 = 节点 id 句柄——runner._node_id 单点)
|
||||
func _runner(db, nodes: Array, anchor_id: int, candidates: Array, n: int = 2):
|
||||
var t := LevelTableScript.new()
|
||||
t.info_visible_layers = n
|
||||
t.rows = [{"level_index": 1, "display_name": "夹具关", "rest_heal_pct": 30, "nodes": nodes}]
|
||||
db.level = t
|
||||
var r = load("res://core/level_runner.gd").new(db, load("res://core/rng_service.gd").new())
|
||||
r.level_row.assign({"level_index": 1, "display_name": "夹具关", "rest_heal_pct": 30, "nodes": nodes})
|
||||
r._graph_mode = graph_mode
|
||||
r.visited.clear()
|
||||
r._candidates.assign(candidates)
|
||||
r.node_index = -1
|
||||
if anchor_id >= 0:
|
||||
for i in nodes.size():
|
||||
if int((nodes[i] as Dictionary).get("node_depth", -1)) == anchor_id:
|
||||
if int((nodes[i] as Dictionary).get("id", -1)) == anchor_id:
|
||||
r.node_index = i
|
||||
break
|
||||
return r
|
||||
@@ -157,15 +167,15 @@ func _test_single_circle(db) -> void:
|
||||
Kit.check("单圈只按锚层取窗:已过近端节点 4(层距 2)仍可见〔fv7〕", vis.has(4))
|
||||
|
||||
|
||||
# ---- ⑤ 单链退化形态(编号空间 = 数组下标)〔fv1〕----
|
||||
# ---- ⑤ 单链退化形态(出度 ≤1 图,编号 = id 句柄)〔fv1〕----
|
||||
|
||||
func _test_single_chain_entry(db) -> void:
|
||||
var nodes := _chain_nodes()
|
||||
var r = _runner(db, nodes, -1, [nodes[0]], false)
|
||||
var r = _runner(db, nodes, -1, [nodes[0]])
|
||||
var vis: Array = r.type_visible_ids()
|
||||
Kit.check("单链退化形态:入口锚层 0,下标 2 可见〔fv1〕", vis.has(2))
|
||||
Kit.check("单链退化形态:层距 N+1(下标 3)不可见〔fv1〕", not vis.has(3))
|
||||
Kit.check("单链编号空间 = 数组下标:Boss 以末位下标 6 入集(非 node_depth 4)〔fv1〕",
|
||||
Kit.check("单链退化形态:入口锚层 0,句柄 2 可见〔fv1〕", vis.has(2))
|
||||
Kit.check("单链退化形态:层距 N+1(句柄 3)不可见〔fv1〕", not vis.has(3))
|
||||
Kit.check("单链编号空间 = 图句柄 id:Boss 以句柄 6 入集(链上层距 4 的战斗节点 4 入雾)〔fv1〕",
|
||||
vis.has(6) and not vis.has(4))
|
||||
|
||||
|
||||
@@ -189,20 +199,37 @@ func _test_n_from_table(db) -> void:
|
||||
# ---- ⑦ 续玩重建同值〔fv7〕----
|
||||
|
||||
func _test_resume_rebuild(db) -> void:
|
||||
var nodes := _graph_nodes()
|
||||
var t := LevelTableScript.new()
|
||||
t.info_visible_layers = 2
|
||||
t.rows = [{"level_index": 1, "display_name": "夹具关", "rest_heal_pct": 30, "nodes": nodes}]
|
||||
t.rows = [{"level_index": 1, "display_name": "夹具关", "rest_heal_pct": 30}]
|
||||
db.level = t
|
||||
var ledger := {"hero_hp": 10, "gp": 0}
|
||||
var resumed = load("res://core/level_runner.gd").new(db, load("res://core/rng_service.gd").new())
|
||||
var ok: bool = resumed.start_level_external(1, ledger, {}, Callable(), {}, false, 0,
|
||||
var ok: bool = resumed.start_level_external(1, ledger, {}, Callable(), {}, false,
|
||||
{"node_id": 2, "visited_ids": [0, 1, 2]})
|
||||
Kit.check_quiet(ok, "续玩用例:resume_state 重建停点成功")
|
||||
if not ok:
|
||||
return
|
||||
Kit.check_quiet(resumed.awaiting_choice(), "续玩用例:重建后处于选路停点")
|
||||
var direct = _runner(db, nodes, 2, [nodes[3]])
|
||||
direct.visited.assign([nodes[0], nodes[1], nodes[2]])
|
||||
# 图源 = 参数化生成图(图 = f(参数档, 种子))——直入对照侧与 resume 侧同吃一张生成图,
|
||||
# 停点态按句柄对齐:锚 = id 2、visited = id 0/1/2、候选 = id 2 出边指向的节点
|
||||
var gnodes: Array = resumed.nodes()
|
||||
var stop: Dictionary = {}
|
||||
var by_id := {}
|
||||
for node in gnodes:
|
||||
var nid: int = int((node as Dictionary).get("id", -1))
|
||||
by_id[nid] = node
|
||||
if nid == 2:
|
||||
stop = node
|
||||
var cand_nodes: Array = []
|
||||
for tid in (stop.get("next", []) as Array):
|
||||
if by_id.has(int(tid)):
|
||||
cand_nodes.append(by_id[int(tid)])
|
||||
var visited_nodes: Array = []
|
||||
for vid in [0, 1, 2]:
|
||||
if by_id.has(vid):
|
||||
visited_nodes.append(by_id[vid])
|
||||
var direct = _runner(db, gnodes, 2, cand_nodes)
|
||||
direct.visited.assign(visited_nodes)
|
||||
Kit.check("续玩重建同值:resume 路径与同停点直入 type_visible_ids() 逐位一致〔fv7〕",
|
||||
resumed.type_visible_ids() == direct.type_visible_ids())
|
||||
@@ -313,15 +313,15 @@ func _meta_settle_and_workshop() -> void:
|
||||
Kit.check("开局传参夹具前置:Fork 侧 a 激活 rule 键非空 + Y=Lv2 行表值 2(防断言空转)",
|
||||
rules_e.has("posture_first_strike") and mp_e.attune_refresh_charges() == 2)
|
||||
var p_new: Dictionary = load("res://presentation/main.gd").build_adventure_params(sv_e, mp_e,
|
||||
{"first_script": true, "treasure_unlocked": {}, "shop_mode": "pool"})
|
||||
{"treasure_unlocked": {"wide_scout_band": true}, "shop_mode": "pool"})
|
||||
Kit.check("主菜单开新冒险参数面:同调装配 = 存档装配集(营地装了什么开局就带什么)",
|
||||
(p_new.get("attunement", []) as Array) == (sv_e.attunement_equipped() as Array))
|
||||
Kit.check("主菜单开新冒险参数面:Y 刷新池 = 当前等级行表值(营地升几档开局就给几次)",
|
||||
int(p_new.get("attune_refresh_y", -1)) == mp_e.attune_refresh_charges())
|
||||
Kit.check("主菜单开新冒险参数面:树 rule 键集 = Fork 激活侧 effects(点哪侧带哪侧)",
|
||||
(p_new.get("tree_rules", []) as Array) == rules_e)
|
||||
Kit.check("主菜单开新冒险参数面:入口特有键(first_script/shop_mode)随 extra 原样并入",
|
||||
bool(p_new.get("first_script", false)) and String(p_new.get("shop_mode", "")) == "pool")
|
||||
Kit.check("主菜单开新冒险参数面:入口特有键(treasure_unlocked/shop_mode)随 extra 原样并入",
|
||||
bool((p_new.get("treasure_unlocked", {}) as Dictionary).get("wide_scout_band", false)) and String(p_new.get("shop_mode", "")) == "pool")
|
||||
var p_resume: Dictionary = load("res://presentation/main.gd").build_adventure_params(sv_e, mp_e,
|
||||
{"treasure_unlocked": {}})
|
||||
Kit.check("主菜单继续上局参数面:三键与开新冒险同源同值(续玩不吃白板开局)",
|
||||
@@ -333,7 +333,7 @@ func _meta_settle_and_workshop() -> void:
|
||||
var adr_res = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(19))
|
||||
var bp_min: Dictionary = {"level_index": 1, "node_id": -1, "hero_hp": 40, "gp": 5,
|
||||
"bonus": {"atk": 0, "max_hp": 0, "armor_class": 0}, "shop_purchased": {},
|
||||
"template_pick": -1, "master_seed": 777, "rng_counter": 3, "keystone": 0,
|
||||
"graph_gen_v2": 1, "master_seed": 777, "rng_counter": 3, "keystone": 0,
|
||||
"visited_ids": []}
|
||||
Kit.check("继续上局受理面:resume_adventure 受理三键参数面(尾裁微批b 受理闭环——受理不再吃白板)",
|
||||
adr_res.resume_adventure(bp_min, p_resume))
|
||||
|
||||
@@ -34,7 +34,7 @@ func _init() -> void:
|
||||
|
||||
# T10——空配置验收闸(三选一池空兜底/商店空货架)
|
||||
var e = load("res://core/adventure_runner.gd").new(db, load("res://core/rng_service.gd").new())
|
||||
e.start_adventure(501, {"first_script": true})
|
||||
e.start_adventure(501)
|
||||
e._curio_pending = {"candidates": []}
|
||||
Kit.check_quiet(e.skip_empty_curio() and e._curio_pending.is_empty(),
|
||||
"空配置闸:三选一池空=停点整体跳过(skip_empty_curio 消化)〔专长批 T10〕")
|
||||
@@ -65,7 +65,7 @@ func _tiers_of(cands: Array) -> Array:
|
||||
|
||||
|
||||
func check_v7(a, db) -> void:
|
||||
if not a.start_adventure(201, {"first_script": true}):
|
||||
if not a.start_adventure(201):
|
||||
Kit.check_quiet(false, "V7 前置:冒险启动")
|
||||
return
|
||||
# 构成①:全池含优秀档以上——曝光槽应出 1 格高档(rank≥excellent)
|
||||
@@ -110,7 +110,7 @@ func check_v7(a, db) -> void:
|
||||
|
||||
|
||||
func check_v6(b, db) -> void:
|
||||
if not b.start_adventure(202, {"first_script": true}):
|
||||
if not b.start_adventure(202):
|
||||
Kit.check_quiet(false, "V6 前置:冒险启动")
|
||||
return
|
||||
# 拿了消耗:直置停点候选后 pick_curio——牌库摘牌单点
|
||||
@@ -139,7 +139,7 @@ func check_v6(b, db) -> void:
|
||||
|
||||
|
||||
func check_v12(m, db) -> void:
|
||||
if not m.start_adventure(401, {"first_script": true}):
|
||||
if not m.start_adventure(401):
|
||||
Kit.check_quiet(false, "V12 前置:冒险启动")
|
||||
return
|
||||
var tier_n := {"common": 0, "excellent": 0, "rare": 0, "legendary": 0}
|
||||
@@ -157,7 +157,7 @@ func check_v12(m, db) -> void:
|
||||
|
||||
|
||||
func check_v8(c, db) -> void:
|
||||
if not c.start_adventure(203, {"first_script": true}):
|
||||
if not c.start_adventure(203):
|
||||
Kit.check_quiet(false, "V8 前置:冒险启动")
|
||||
return
|
||||
# 概率位与 cap 两属性独立:改 cap 不动牌库(概率位)、改牌库不动 cap 字段
|
||||
|
||||
@@ -184,7 +184,7 @@ func _smoke_save() -> void:
|
||||
"level_index": 2, "node_id": 5, "hero_hp": 13,
|
||||
"bonus": {"atk": 2, "max_hp": 5, "armor_class": 1},
|
||||
"gp": 17, "shop_purchased": {"hp_small": 2, "atk_small": 1},
|
||||
"template_pick": 3,
|
||||
"graph_gen_v2": 1,
|
||||
"master_seed": 424242, "rng_counter": 38, "keystone": 6,
|
||||
"visited_ids": [0, 2, 5],
|
||||
"aux_items": [{"item_id": "aux_scout", "source_class": "reward_node",
|
||||
@@ -205,7 +205,7 @@ func _smoke_save() -> void:
|
||||
var bp_shop: Dictionary = bp.get("shop_purchased", {})
|
||||
Kit.check("断点七字段往返:商店已购档",
|
||||
int(bp_shop.get("hp_small", -1)) == 2 and int(bp_shop.get("atk_small", -1)) == 1)
|
||||
Kit.check("断点七字段往返:已抽模板", int(bp.get("template_pick", -1)) == 3)
|
||||
Kit.check("断点七字段往返:生成图代差标记键", int(bp.get("graph_gen_v2", 0)) == 1)
|
||||
Kit.check("断点附随件往返:主种子/计数器/钥石累计",
|
||||
int(bp.get("master_seed", -1)) == 424242 and int(bp.get("rng_counter", -1)) == 38
|
||||
and int(bp.get("keystone", -1)) == 6)
|
||||
@@ -237,7 +237,7 @@ func _smoke_save() -> void:
|
||||
fv2f.store_string(JSON.stringify({"version": 2, "meta": {"keystone_balance": 7},
|
||||
"breakpoint": {"level_index": 1, "node_id": -1, "hero_hp": 40, "gp": 5,
|
||||
"bonus": {"atk": 0, "max_hp": 0, "armor_class": 0}, "shop_purchased": {},
|
||||
"template_pick": -1, "master_seed": 777, "rng_counter": 3, "keystone": 0,
|
||||
"graph_gen_v2": 1, "master_seed": 777, "rng_counter": 3, "keystone": 0,
|
||||
"visited_ids": []}}))
|
||||
fv2f.close()
|
||||
Kit.check("v2 旧档迁移 v3:断点域与 meta 域原样保留(缺新附随件=合法空态)",
|
||||
|
||||
@@ -72,14 +72,6 @@ static func _errs_has_cross_field(errs: Array) -> bool:
|
||||
return false
|
||||
|
||||
|
||||
# 该关 Boss 节点深度(单终点图的终点编号——数据驱动,禁写死坐标)
|
||||
func _boss_depth(level_index: int) -> int:
|
||||
for n in _level_active_nodes(level_index):
|
||||
if String(n.get("type", "")) == "boss":
|
||||
return int(n.get("node_depth", -1))
|
||||
return -1
|
||||
|
||||
|
||||
# 首遇带数据驱动定位(契约 8):该 (L, battle, enemy_id) 的最低 depth_band,禁写死坐标数字
|
||||
func _first_encounter_band(level_index: int, enemy_id: String) -> int:
|
||||
var best := -1
|
||||
@@ -94,17 +86,6 @@ func _first_encounter_band(level_index: int, enemy_id: String) -> int:
|
||||
return best
|
||||
|
||||
|
||||
# 模板感知取节点表(row.templates 非空取模板 0=现行固定图,保持既有断言语义)
|
||||
func _level_active_nodes(level_index: int) -> Array:
|
||||
for row in _db.level.rows:
|
||||
if int(row.level_index) == level_index:
|
||||
var tpls: Array = row.get("templates", [])
|
||||
if not tpls.is_empty():
|
||||
return tpls[0].get("nodes", [])
|
||||
return row.get("nodes", [])
|
||||
return []
|
||||
|
||||
|
||||
# ① 冒险表(单行全局 Rule 4 豁免——建表与接线闸;TP0 令牌池:配额结构/件值对/池总量域检)
|
||||
func _smoke_adventure_table() -> void:
|
||||
print("--- 冒险表(AdventureTable——令牌池行值单点:回血/配额/件值对/同调位)---")
|
||||
|
||||
@@ -29,7 +29,7 @@ func _run() -> void:
|
||||
quit(Kit.finish("level_map_info_fog_test"))
|
||||
|
||||
|
||||
# 白盒 runner:夹具行集注入 db.level + level_row,入口停点态(node_index=-1,候选=入边首节点)
|
||||
# 白盒 runner:夹具行集注入 db.level + level_row,入口停点态(node_index=-1,候选=首层首节点句柄 1)
|
||||
func _runner(db, nodes: Array, candidates: Array, n: int = 2):
|
||||
var t := LevelTableScript.new()
|
||||
t.info_visible_layers = n
|
||||
@@ -37,7 +37,6 @@ func _runner(db, nodes: Array, candidates: Array, n: int = 2):
|
||||
db.level = t
|
||||
var r = load("res://core/level_runner.gd").new(db, load("res://core/rng_service.gd").new())
|
||||
r.level_row.assign({"level_index": 1, "display_name": "夹具关", "rest_heal_pct": 30, "nodes": nodes})
|
||||
r._graph_mode = true
|
||||
r.node_index = -1
|
||||
r._candidates.assign(candidates)
|
||||
return r
|
||||
@@ -46,7 +45,7 @@ func _runner(db, nodes: Array, candidates: Array, n: int = 2):
|
||||
# ---- ① 范围外只见结构:雾下节点类型图标与文字均不绘制〔fv2〕----
|
||||
|
||||
func _test_fog_hides_type() -> void:
|
||||
var node := {"node_depth": 3, "type": "battle"}
|
||||
var node := {"id": 3, "type": "battle", "depth": 3, "next": []}
|
||||
var shown: Dictionary = ScreenScript.node_display(node, 2, [2], false, false)
|
||||
Kit.check("可见集内节点:图标/类型文字均绘制〔fv2〕",
|
||||
bool(shown["draw_icon"]) and bool(shown["draw_label"]))
|
||||
@@ -60,24 +59,24 @@ func _test_fog_hides_type() -> void:
|
||||
|
||||
func _test_boss_exempt_and_multi_boss() -> void:
|
||||
var type_vis: Array = [1, 7, 9] # 双 Boss 图:7/9 均在可见集
|
||||
var boss_a: Dictionary = ScreenScript.node_display({"node_depth": 7, "type": "boss"}, 7, type_vis, false, false)
|
||||
var boss_b: Dictionary = ScreenScript.node_display({"node_depth": 9, "type": "boss"}, 9, type_vis, false, false)
|
||||
var boss_a: Dictionary = ScreenScript.node_display({"id": 7, "type": "boss", "depth": 7, "next": []}, 7, type_vis, false, false)
|
||||
var boss_b: Dictionary = ScreenScript.node_display({"id": 9, "type": "boss", "depth": 9, "next": []}, 9, type_vis, false, false)
|
||||
Kit.check("多终点:两个 Boss 节点均带 Boss 描边〔fv3〕",
|
||||
bool(boss_a["boss_border"]) and bool(boss_b["boss_border"]))
|
||||
Kit.check("Boss 不入雾:可见集与图标/文字三通道均在〔fv3〕",
|
||||
bool(boss_a["draw_icon"]) and bool(boss_a["draw_label"]) and bool(boss_b["draw_icon"]))
|
||||
var far_boss: Dictionary = ScreenScript.node_display({"node_depth": 9, "type": "boss"}, 9, [1], false, false)
|
||||
var far_boss: Dictionary = ScreenScript.node_display({"id": 9, "type": "boss", "depth": 9, "next": []}, 9, [1], false, false)
|
||||
Kit.check("Boss 豁免独立于可见集:不在 type_vis 的 Boss 仍出示类型〔fv3〕",
|
||||
bool(far_boss["draw_icon"]) and bool(far_boss["draw_label"]))
|
||||
var fogged_battle: Dictionary = ScreenScript.node_display({"node_depth": 5, "type": "battle"}, 5, type_vis, false, false)
|
||||
var fogged_battle: Dictionary = ScreenScript.node_display({"id": 5, "type": "battle", "depth": 5, "next": []}, 5, type_vis, false, false)
|
||||
Kit.check("Boss 豁免不扩散:雾下非 Boss 仍隐类型〔fv3〕", not bool(fogged_battle["draw_icon"]))
|
||||
|
||||
|
||||
# ---- ③ 节点内详情图上从不显示〔fv5〕----
|
||||
|
||||
func _test_details_never_shown() -> void:
|
||||
var plain := {"node_depth": 3, "type": "battle"}
|
||||
var loaded := {"node_depth": 3, "type": "battle", "enemy_id": "goblin_secret", "reward_id": "reward_secret"}
|
||||
var plain := {"id": 3, "type": "battle", "depth": 3, "next": [4]}
|
||||
var loaded := {"id": 3, "type": "battle", "depth": 3, "next": [4], "enemy_id": "goblin_secret", "reward_id": "reward_secret"}
|
||||
var a: Dictionary = ScreenScript.node_display(plain, 3, [3], false, true)
|
||||
var b: Dictionary = ScreenScript.node_display(loaded, 3, [3], false, true)
|
||||
var same := true
|
||||
@@ -92,7 +91,7 @@ func _test_details_never_shown() -> void:
|
||||
# ---- ④ label_text 三态文案〔fv2〕----
|
||||
|
||||
func _test_label_three_states() -> void:
|
||||
var node := {"node_depth": 3, "type": "battle"}
|
||||
var node := {"id": 3, "type": "battle", "depth": 3, "next": [4]}
|
||||
Kit.check("当前位文案=「当前位置」〔fv2〕",
|
||||
String(ScreenScript.node_display(node, 3, [3], true, false)["label_text"]) == "当前位置")
|
||||
Kit.check("候选文案=「战斗 · 可选」〔fv2〕",
|
||||
@@ -103,9 +102,10 @@ func _test_label_three_states() -> void:
|
||||
|
||||
# ---- 大图(11 层×6 槽含双 Boss)画布拟合与布局包络〔fv8〕----
|
||||
|
||||
# 大图夹具:编号按 12 层×6 槽构造,层 0 入口、末层双 Boss;含层 1 首节点→第 3 层节点的跨层跳边。
|
||||
# 跳边目标按 BFS 最短路径提前一层,其后继链随之提前——布局落 11 层(最大槽 7:
|
||||
# 跳边目标并入第 2 层槽组)。断言按布局实测取最大层/最大槽,画布公式与之同源。
|
||||
# 大图夹具:按 12 层×6 槽构造(id = 图内句柄 = 构造序号;depth 随构造序号严格前进——生成图连线铁律),
|
||||
# 层 0 入口、末层双 Boss(id 61/62,句柄互异、next 恒空);含层 1 首节点→第 3 层节点的跨层长跳边。
|
||||
# 跳边目标按 BFS 最短路径提前一层,其后继链随之逐层提前(目标并入第 2 层槽组)——布局落 11 层。
|
||||
# 断言按布局实测取最大层/最大槽,画布公式与之同源。
|
||||
func _big_graph_nodes() -> Array:
|
||||
var layers: Array = []
|
||||
var total := 0
|
||||
@@ -118,30 +118,25 @@ func _big_graph_nodes() -> Array:
|
||||
total += 1
|
||||
layers.append(ids)
|
||||
var nodes: Array = []
|
||||
var jump_from := -1
|
||||
var jump_to := -1
|
||||
for l in tiers:
|
||||
for i in layers[l].size():
|
||||
var nid: int = layers[l][i]
|
||||
var n := {"node_depth": nid}
|
||||
if l == 0:
|
||||
n["type"] = "battle"
|
||||
n["next"] = layers[1].duplicate() # 入口扇出到全层(层宽 6)
|
||||
elif l == tiers - 1:
|
||||
n["type"] = "boss"
|
||||
n["next"] = []
|
||||
var t := "battle"
|
||||
var next_ids: Array = []
|
||||
if l == tiers - 1:
|
||||
t = "boss"
|
||||
elif l == 0:
|
||||
next_ids = layers[1].duplicate() # 入口扇出到全层(层宽 6)
|
||||
else:
|
||||
if i % 5 == 0:
|
||||
n["type"] = "reward" if l % 2 == 0 else "rest"
|
||||
else:
|
||||
n["type"] = "battle"
|
||||
t = "reward" if l % 2 == 0 else "rest"
|
||||
var w2: int = layers[l + 1].size()
|
||||
n["next"] = [layers[l + 1][i % w2], layers[l + 1][(i + 1) % w2]]
|
||||
nodes.append(n)
|
||||
jump_from = layers[1][0]
|
||||
jump_to = layers[3][2]
|
||||
next_ids = [layers[l + 1][i % w2], layers[l + 1][(i + 1) % w2]]
|
||||
nodes.append({"id": nid, "type": t, "depth": nid, "next": next_ids})
|
||||
var jump_from: int = layers[1][0]
|
||||
var jump_to: int = layers[3][2]
|
||||
for n in nodes:
|
||||
if int(n["node_depth"]) == jump_from:
|
||||
if int(n["id"]) == jump_from:
|
||||
(n["next"] as Array).append(jump_to)
|
||||
break
|
||||
return nodes
|
||||
@@ -188,9 +183,9 @@ func _test_big_graph_fit(db) -> void:
|
||||
var boss_ids: Array = []
|
||||
for node in nodes:
|
||||
if String(node["type"]) == "boss":
|
||||
boss_ids.append(int(node["node_depth"]))
|
||||
boss_ids.append(int(node["id"]))
|
||||
Kit.check("大图呈现:两个终点 Boss 均有描边标记〔fv8〕",
|
||||
disp.has(boss_ids[0]) and disp.has(boss_ids[1])
|
||||
and bool(disp[boss_ids[0]]["boss_border"]) and bool(disp[boss_ids[1]]["boss_border"]))
|
||||
Kit.check("大图呈现:入口停点远处战斗节点(编号 32,层 6)类型入雾〔fv8〕", not bool(disp[32]["draw_icon"]))
|
||||
Kit.check("大图呈现:入口停点远处战斗节点(句柄 32,层 6)类型入雾〔fv8〕", not bool(disp[32]["draw_icon"]))
|
||||
screen.queue_free()
|
||||
Reference in new issue
Block a user