diff --git a/game/tests/integration/node_map_templates_test.gd b/game/tests/integration/node_map_templates_test.gd index eb662953..45d8a2fc 100644 --- a/game/tests/integration/node_map_templates_test.gd +++ b/game/tests/integration/node_map_templates_test.gd @@ -59,8 +59,14 @@ func _smoke_generated_map_determinism() -> void: Kit.check("冒险模式起关受理(入口停点)", runner2.start_level_external(3, {"hero_hp": 20, "gp": 0}, {}, Callable(self, "_smoke_rest_stub"), {}, true)) Kit.check("起关产图零计数器消费(同图不依赖取数号位)", rng_c.counter() == counter_before) - Kit.check("入口停点候选 = 生成图全部无入边节点(单入口时恰 1)", - (runner2.next_candidates() as Array).size() >= 1) + var params3: Dictionary = _db.level_gen_params_row(3) + var entry_count: int = int(params3.get("entry_count", -1)) + Kit.check("入口停点候选数=参数档入口个数(L3 档 %d——多入口首跳即选路,禁写死单入口)" % entry_count, + entry_count >= 1 and (runner2.next_candidates() as Array).size() == entry_count) + if entry_count >= 2: + var eids: Array = runner2.next_candidate_ids() + Kit.check("第二入口经句柄显式进入(句柄 %d)" % int(eids[1]), + runner2.pick_next_node(int(eids[1])) and int(runner2.current_node_id()) == int(eids[1])) # ④ 开局种子承载:新档(首局)种子 42 两次开局逐位同图,且与生成器直取一致 DirAccess.remove_absolute(_bp_path) # 测试隔离——上轮落盘残留先删,断点面从零档起 var sv_w = SaveManagerScript.new(_bp_path) diff --git a/game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd b/game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd index 5bd587a5..066a3ad0 100644 --- a/game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd +++ b/game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd @@ -339,9 +339,6 @@ func _first_encounter_band(level_index: int, enemy_id: String) -> int: return best -# 模板感知取节点表(row.templates 非空取模板 0=现行固定图,保持既有断言语义) - - # 生成图取数面(图 = f(关卡行参数档, 种子 42)——与 runner 起关同图;节点键 id/type/depth/next) func _level_active_nodes(level_index: int) -> Array: var rng = load("res://core/rng_service.gd").new() diff --git a/game/tests/unit/core/adventure_feats_test.gd b/game/tests/unit/core/adventure_feats_test.gd index bc809a18..d6ec9e6e 100644 --- a/game/tests/unit/core/adventure_feats_test.gd +++ b/game/tests/unit/core/adventure_feats_test.gd @@ -163,54 +163,33 @@ func _smoke_adventure() -> void: 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: Dictionary = _drive_to_pref(autoplay, "reward") + Kit.check("奖励分支点选→三选一候选可取且停点在途(候选 %s 件)" % str((rw.get("cands", []) as Array).size()), + bool(rw.get("pending", false)) and (rw.get("cands", []) as Array).size() >= 1) var rw_pc: Dictionary = {} - for c in rw_cands: + for c in (rw.get("cands", []) as Array): 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 + if rw_pc.is_empty() and not (rw.get("cands", []) as Array).is_empty(): + rw_pc = (rw.get("cands", []) as Array)[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) + if not rw_pc.is_empty() and rw.get("adr") != null: + rw_pick_ok = rw["adr"].apply_node_curio(String(rw_pc.get("kind", "")), String(rw_pc.get("tier", "")), String(rw_pc.get("feat_id", ""))) + Kit.check("奖励三选一点选兑付受理(件=%s)" % String(rw_pc.get("kind", "")), rw_pick_ok) + var enc_run: Dictionary = _drive_to_pref(autoplay, "battle") + var enc_probe: Dictionary = enc_run.get("enc", {}) as Dictionary + var node_probe: Dictionary = enc_run.get("node", {}) as Dictionary + # 定位信息两样(契约 2):战斗侧收到的 node_depth=节点深度,且命中节点句柄≠深度—— + # 变异「enc 取句柄」必红(V6) + Kit.check("进战载荷两样=关卡序号+节点深度(实测 depth=%s 句柄=%s)〔v6〕" % [str(enc_probe.get("node_depth", -1)), str(node_probe.get("id", -1))], + not enc_probe.is_empty() + and int(enc_probe.get("node_depth", -1)) == int(node_probe.get("depth", -1)) + and int(enc_probe.get("level_index", -1)) == 1 + and int(node_probe.get("id", -1)) != int(node_probe.get("depth", -1))) var node_resolves := 0 for r0 in resolves: if String((r0 as Dictionary).get("context", "")) == "reward_node": @@ -828,6 +807,50 @@ func _adv_drive_to_shop(a, st: Dictionary, autoplay) -> bool: # 手工场景状态袋(_drive_level/_autoplay_battle 消费的最小字段族——run_adventure st 的同构子集) +# 真实选路驱动到偏好支路:赢下途中战斗,遇候选含目标类型即显式点选(reward=首个奖励节点、 +# battle=首个句柄≠深度的战斗节点);返回 {node, enc, cands, pending, adr} +func _drive_to_pref(autoplay, prefer: String) -> Dictionary: + var adr = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(42)) + adr.set_hero_override(900, 400) + var out := {"node": {}, "enc": {}, "cands": [] as Array, "pending": false, "adr": adr} + if not adr.start_adventure(42): + return out + var r = adr.runner() + var st := _adv_st() + var guard := 30 + while r != null and guard > 0: + guard -= 1 + if r.awaiting_choice(): + var hit: Dictionary = {} + for c in r.next_candidates(): + var cd := c as Dictionary + if prefer == "reward" and String(cd.get("type", "")) == "reward": + hit = cd + break + if prefer == "battle" and String(cd.get("type", "")) == "battle" and int(cd.get("id", -1)) != int(cd.get("depth", -1)): + hit = cd + break + if not hit.is_empty(): + r.pick_next_node(int(hit.get("id", -1))) + out["node"] = hit + if prefer == "battle": + out["enc"] = r.current_battle().encounter + else: + var cands: Array = (adr.node_curio_candidates() as Dictionary).get("candidates", []) as Array + adr.present_node_curio(cands) + out["cands"] = cands + out["pending"] = not (adr._node_curio_pending as Dictionary).is_empty() + return out + var ids: Array = r.next_candidate_ids() + if ids.is_empty(): + return out + r.pick_next_node(int(ids[0])) + continue + var b = r.current_battle() + if b == null: + return out + autoplay._autoplay_battle(r, b, st, null) + return out func _adv_st() -> Dictionary: return {"result": "", "hp_left": 0, "battles": 0, "rounds": 0, "hero_dmg": 0, "rest_healed": 0, "hp_before_rest": -1, "defeat_node_index": -1, "defeat_node_type": "",