From 3849211621765d330bd2dc39fa84f279e964e46f Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Sat, 24 Oct 2026 19:09:04 +0800 Subject: [PATCH] =?UTF-8?q?test(=E8=BF=AD=E4=BB=A335=E5=8D=A1=E2=91=A1T3?= =?UTF-8?q?=20=E8=A1=A5=E5=8F=A3):=20V2=20=E5=A4=9A=E5=85=A5=E5=8F=A3?= =?UTF-8?q?=E4=B8=8E=20V6=20=E5=AE=9A=E4=BD=8D=E4=BF=A1=E6=81=AF=E4=B8=A4?= =?UTF-8?q?=E6=9D=A1=E7=9C=9F=E5=AE=9E=E5=85=A5=E5=8F=A3=E6=96=AD=E8=A8=80?= =?UTF-8?q?=E8=A1=A5=E4=BD=8D=EF=BC=8B=E6=AD=BB=E6=B3=A8=E9=87=8A=E6=B8=85?= =?UTF-8?q?=E6=89=AB=E2=80=94=E2=80=94=E2=91=A0=20node=5Fmap=5Ftemplates?= =?UTF-8?q?=20=E5=85=A5=E5=8F=A3=E5=81=9C=E7=82=B9=E5=80=99=E9=80=89?= =?UTF-8?q?=E6=95=B0=E6=94=B9=E5=88=A4=E3=80=8C=EF=BC=9D=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E6=A1=A3=20entry=5Fcount=E3=80=8D=EF=BC=88L3=20=E6=A1=A3?= =?UTF-8?q?=E5=AE=9E=E6=B5=8B=202=EF=BC=89=E5=B9=B6=E5=8A=A0=E7=AC=AC?= =?UTF-8?q?=E4=BA=8C=E5=85=A5=E5=8F=A3=E5=8F=A5=E6=9F=84=E6=98=BE=E5=BC=8F?= =?UTF-8?q?=E8=BF=9B=E5=85=A5=E6=96=AD=E8=A8=80=EF=BC=88=E5=8F=98=E5=BC=82?= =?UTF-8?q?=E3=80=8C=E5=80=99=E9=80=89=E5=86=99=E6=AD=BB=E5=8D=95=E5=85=A5?= =?UTF-8?q?=E5=8F=A3=E3=80=8D=E7=9A=84=E6=8B=A6=E6=88=AA=E9=9D=A2=EF=BC=8C?= =?UTF-8?q?=E5=8E=9F=20>=3D1=20=E5=86=99=E6=B3=95=E6=8B=A6=E4=B8=8D?= =?UTF-8?q?=E4=BD=8F=EF=BC=89=EF=BC=9B=E2=91=A1=20adventure=5Ffeats=20?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=20=5Fdrive=5Fto=5Fpref=20=E7=9C=9F=E5=AE=9E?= =?UTF-8?q?=E9=80=89=E8=B7=AF=E9=A9=B1=E5=8A=A8=EF=BC=88=E6=BB=A1=E9=85=8D?= =?UTF-8?q?=E8=B5=A2=E9=80=94=E4=B8=AD=E6=88=98=E2=86=92=E5=B2=94=E5=8F=A3?= =?UTF-8?q?=E6=8C=89=E5=81=8F=E5=A5=BD=E7=82=B9=E9=80=89=EF=BC=89=EF=BC=8C?= =?UTF-8?q?=E6=8D=AE=E6=AD=A4=E8=90=BD=E4=B8=89=E6=96=AD=E8=A8=80=EF=BC=9A?= =?UTF-8?q?=E5=A5=96=E5=8A=B1=E6=94=AF=E8=B7=AF=E7=BB=8F=E5=81=9C=E5=91=88?= =?UTF-8?q?=E4=B8=89=E9=80=89=E4=B8=80=E5=80=99=E9=80=89=E5=9C=A8=E9=80=94?= =?UTF-8?q?=E3=80=81=E7=82=B9=E9=80=89=E5=85=91=E4=BB=98=E5=8F=97=E7=90=86?= =?UTF-8?q?=E3=80=81=E8=BF=9B=E6=88=98=E8=BD=BD=E8=8D=B7=20node=5Fdepth?= =?UTF-8?q?=EF=BC=9D=E8=8A=82=E7=82=B9=E6=B7=B1=E5=BA=A6=E4=B8=94=E5=91=BD?= =?UTF-8?q?=E4=B8=AD=E8=8A=82=E7=82=B9=E5=8F=A5=E6=9F=84=E2=89=A0=E6=B7=B1?= =?UTF-8?q?=E5=BA=A6=EF=BC=88=E5=8F=98=E5=BC=82=E3=80=8Cenc=20=E5=8F=96?= =?UTF-8?q?=E5=8F=A5=E6=9F=84=E3=80=8D=E6=8B=A6=E6=88=AA=E9=9D=A2=E3=80=94?= =?UTF-8?q?v6=E3=80=95=EF=BC=89=EF=BC=9B=E2=91=A2=20round=5Fflow=20?= =?UTF-8?q?=E6=A8=A1=E6=9D=BF=E6=97=B6=E4=BB=A3=E5=AD=A4=E5=84=BF=E6=B3=A8?= =?UTF-8?q?=E9=87=8A=E5=88=A0=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../integration/node_map_templates_test.gd | 10 +- .../round_flow_skill_pool_test.gd | 3 - game/tests/unit/core/adventure_feats_test.gd | 107 +++++++++++------- 3 files changed, 73 insertions(+), 47 deletions(-) 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": "",