From 04783faee4cb453470f1b8953238324f77a8091b Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Fri, 30 Oct 2026 06:08:46 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8D=A1=E2=91=A2T2(=E6=B5=8B=E9=80=9F?= =?UTF-8?q?=E5=BE=AA=E7=8E=AF=E6=89=B9v1.1):=20=E5=8F=8C=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E5=AF=B9=E6=8B=8D=E5=A5=97=E4=BB=B6=E2=80=94=E2=80=94AwaitLoop?= =?UTF-8?q?Runner(=E7=8E=B0=E8=A1=8Cawait=E5=BD=A2=E6=80=81=E9=80=90?= =?UTF-8?q?=E5=AD=97=E5=89=AF=E6=9C=AC=C2=B7=E6=94=B6=E5=B0=BErun=E5=B0=BE?= =?UTF-8?q?=C2=B7=E7=94=B3=E6=8A=A5=E5=B7=AE=E5=BC=82=E5=94=AF=E4=B8=80=3D?= =?UTF-8?q?stats=E6=9A=B4=E9=9C=B2out=5Fstats)=EF=BC=8BFakePanel/FakeDrive?= =?UTF-8?q?r(wd8=E5=90=8C=E5=BD=A2=C2=B7=E5=B8=A7=E8=AE=A1=E6=95=B0?= =?UTF-8?q?=E7=BB=8F=E5=AE=BF=E4=B8=BBCallable)=EF=BC=8B=E4=BA=94=E9=9D=A2?= =?UTF-8?q?=E5=AF=B9=E6=8B=8D=E6=96=AD=E8=A8=80=E3=80=94=E6=B5=8B=E9=80=9F?= =?UTF-8?q?V1-V5=E3=80=95(=E5=B8=A7=E5=BA=8F=E5=88=97=E7=9B=B8=E5=AF=B9?= =?UTF-8?q?=E5=81=8F=E7=A7=BB=E9=80=90=E4=BD=8D+=E7=9B=B8=E9=82=BB?= =?UTF-8?q?=E5=B7=AE=E6=81=92=E4=B8=80/=E8=BF=90=E8=A1=8C=E6=80=81?= =?UTF-8?q?=E5=BA=8F=E5=88=97=E5=90=8C=E6=9E=84/=E8=BF=9B=E5=BA=A6?= =?UTF-8?q?=E4=B8=B2=E9=80=90=E4=BD=8D/stats=E5=85=AD=E9=94=AE=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E5=8C=96=E9=87=8D=E7=AE=97/=E6=8A=A5=E5=91=8A?= =?UTF-8?q?=E5=85=A8=E6=96=87=E9=80=90=E5=AD=97)=C2=B7n=3D3=E5=B8=B8?= =?UTF-8?q?=E8=A7=84+n=3D0=E9=80=80=E5=8C=96=E4=B8=A4=E6=A1=88;=E4=B8=B2?= =?UTF-8?q?=E8=A1=8C=E9=A9=B1=E5=8A=A8(GDScript=E5=8D=8F=E7=A8=8B=E5=8A=A8?= =?UTF-8?q?=E6=80=81=E8=B0=83=E7=94=A8=E5=BC=BA=E5=88=B6await=C2=B7?= =?UTF-8?q?=E6=97=A0=E5=B9=B6=E8=A1=8C=E5=90=AF=E5=8A=A8=E5=BD=A2=E6=80=81?= =?UTF-8?q?)+=E7=9B=B8=E5=AF=B9=E5=B8=A7=E5=81=8F=E7=A7=BB=E5=BD=92?= =?UTF-8?q?=E4=B8=80+=E6=A0=91=E5=B0=B1=E7=BB=AA=E5=B8=A7await(=5Finit?= =?UTF-8?q?=E6=97=B6=E7=82=B9root=E6=9C=AA=E6=8C=82=E6=A0=91=E5=AE=9E?= =?UTF-8?q?=E6=B5=8B=E2=80=94=E2=80=94=E9=9D=A2=E6=9D=BFget=5Ftree?= =?UTF-8?q?=E9=A6=96=E5=B8=A7=E5=90=8E=E6=89=8D=E6=9C=89=E6=95=88);manifes?= =?UTF-8?q?t=E5=A2=9E=E8=A1=8C;10=E6=96=AD=E8=A8=80=E5=85=A8=E7=BB=BF+wd8?= =?UTF-8?q?=E5=A4=8D=E8=B7=91=E7=BB=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- game/tests/run_all.gd | 1 + .../tune_sim_batch_parity_test.gd | 245 ++++++++++++++++++ 2 files changed, 246 insertions(+) create mode 100644 game/tests/unit/presentation/tune_sim_batch_parity_test.gd diff --git a/game/tests/run_all.gd b/game/tests/run_all.gd index 93e05bd1..f0df8b67 100644 --- a/game/tests/run_all.gd +++ b/game/tests/run_all.gd @@ -186,6 +186,7 @@ func _suites() -> Array: {"path": "res://tests/unit/core/level_map_type_vis_test.gd", "expect": 0}, # 信息迷雾——类型可见性(层距 N+Boss/候选豁免+单圈+N 读表+续玩同值) {"path": "res://tests/unit/presentation/pacing_queue_test.gd", "expect": 0}, # AC⑤/T9——排空零分叉/零丢行/延迟双态 {"path": "res://tests/unit/presentation/standee_fx_sweep_test.gd", "expect": 0}, # 呈现域缺陷扫掠(d2 蓄力档/d4 庆祝句柄/d5+d8 互斥/d12 站桩按 id 解析) + {"path": "res://tests/unit/presentation/tune_sim_batch_parity_test.gd", "expect": 0}, # 测速循环批双实现对拍(帧索引/运行态/进度串/stats/报告五面——信号步进 vs await 基线) {"path": "res://tests/unit/presentation/standee_present_values_test.gd", "expect": 0}, # 战斗一屏样张批 V5–V9(血条危险红/名字行素底不压地台/徽标 FS_HINT/护甲 PNG 绑定/素材入包) {"path": "res://tests/unit/presentation/reward_overlay_test.gd", "expect": 0}, # 奖励节点 UI 直验(盲区补漏) {"path": "res://tests/unit/presentation/level_map_info_fog_test.gd", "expect": 0}, # 信息迷雾——呈现显隐/Boss 豁免/详情零显示/大图拟合 diff --git a/game/tests/unit/presentation/tune_sim_batch_parity_test.gd b/game/tests/unit/presentation/tune_sim_batch_parity_test.gd new file mode 100644 index 00000000..b44b9c6f --- /dev/null +++ b/game/tests/unit/presentation/tune_sim_batch_parity_test.gd @@ -0,0 +1,245 @@ +# 双实现对拍配对套件(注册面批同代测速循环批 v1.1——帧相位等价的执行化证据): +# 产品码(process_frame 信号步进状态机)vs 夹具(现行 await 形态逐字副本——循环内 await+收尾四语句在 run() 尾)。 +# 五面对拍(帧索引序列/运行态序列/进度串序列/stats 六键/报告全文)逐位相等= +# 每步恰一帧+相位同点+输出逐字的当轮真实执行证据(测量语义逐点等价四判据的落码面)。 +# 拓扑钉死:测试宿主先连 process_frame 帧计数、后启动两形态——两形态下「计数先于步进体」相对次序同构,序列可比。 +# n=3 常规案+n=0 退化案(同步收尾零帧——_mean/_p90 空守卫渲染面)。 +# 运行:godot --headless --path game --script res://tests/unit/presentation/tune_sim_batch_parity_test.gd(expect 0) +extends SceneTree + +const Kit := preload("res://tests/unit/test_kit.gd") +const TuneSimScript := preload("res://presentation/tune_sim_controller.gd") + + +# 假调试面板(wd8 同形 setter 面+进度/运行态/报告三记录数组供序列对拍) +class FakePanel extends Node: + var running := false + var progress := "" + var report := "" + var atk := 0 + var hp := 0 + var progress_log: Array[String] = [] + var report_log: Array[String] = [] + var running_log: Array[bool] = [] + + func atk_pct() -> int: + return atk + + func hp_pct() -> int: + return hp + + func set_sim_running(r: bool) -> void: + running = r + running_log.append(r) + + func set_sim_progress(t: String) -> void: + progress = t + progress_log.append(t) + + func set_sim_report(t: String) -> void: + report = t + report_log.append(t) + + +# 假自动策略(固定返回值两档 victory/defeat 交替——wd8 :53-55 同款取值,零随机; +# 逐局记录帧索引与运行态读数——帧索引经宿主注入的计数 Callable 读取) +class FakeDriver extends RefCounted: + var ctrl + var frame_counter: Callable # 宿主帧计数(0 参 Callable——拓扑钉死:计数先于步进体) + var seen: Array[bool] = [] + var frames: Array[int] = [] + var calls := 0 + + func run_level(_db: Variant, _seed: int, _level: int, _atk: int, _hp: int, _other: bool, _logger: Variant) -> Dictionary: + calls += 1 + seen.append(ctrl.is_running()) + frames.append(int(frame_counter.call())) + if calls % 2 == 1: + return {"hero_dmg": 12.0, "rounds": 4, "result": "victory", "hp_left": 3.0, "hp_at_boss": 5.0} + return {"hero_dmg": 6.0, "rounds": 2, "result": "defeat", + "defeat_node_index": 3, "defeat_node_type": "battle", "hp_left": 0.0, "hp_at_boss": 0.0} + + +# 假配置库(TuneSimController 只读 db.run.action_economy.ap_per_round——wd8 同款最小域对象图) +class FakeActionEconomy: + var ap_per_round := 2 + + +class FakeRun: + var action_economy := FakeActionEconomy.new() + + +class FakeDB: + var run := FakeRun.new() + + +# 现行 await 形态逐字副本(tune_sim_controller.gd run(n) 改形前形态——循环内 await+收尾四语句在 run() 尾; +# 全仓唯一在测夹具——产品码禁留第二实现,tests/ 不在 S13/S8/S9/S10 扫描面、夹具内循环 await 合法在册)。 +# 与产品码新形态的申报差异仅一处:批末把 stats 六键束存 out_stats 成员供对拍读取 +# (产品码 stats 经报告公开面观测——内部字段直查受控例外申报在卡 §17)。 +class AwaitLoopRunner extends RefCounted: + const SEED_BASE := 4200 + const ParityLogger := preload("res://presentation/playtest_logger.gd") + var db + var panel + var _base_hp := 0 + var _driver = null + var _running := false + var out_stats: Dictionary = {} + + func _init(db_ref, panel_ref, base_hp: int, driver = null) -> void: + db = db_ref + panel = panel_ref + _base_hp = base_hp + _driver = driver + + func is_running() -> bool: + return _running + + func run(n: int) -> void: + _running = true + var logger = ParityLogger.new() + logger.ap_per_round = int(db.run.action_economy.ap_per_round) + logger.start_session(SEED_BASE) + panel.set_sim_running(true) + panel.set_sim_progress("模拟中:0/%d …" % n) + var stats := {"wins": 0, "dmg_all": [], "hp_left_win": [], "hp_boss": [], "rounds_all": [], "death_nodes": {}} + var autoplay = _driver + for i in range(n): + _run_one_level(autoplay, stats, i, panel, logger) + panel.set_sim_progress("模拟中:%d/%d …" % [i + 1, n]) + await panel.get_tree().process_frame + logger.close_session() + panel.set_sim_report(HudPresenter.tune_sim_report(n, panel.atk_pct(), panel.hp_pct(), stats, _base_hp)) + panel.set_sim_running(false) + _running = false + out_stats = stats + + func _run_one_level(autoplay, stats: Dictionary, i: int, panel, logger) -> void: # type-hint: ignore(驱动/面板/日志件鸭子型注入——同产品码构造缝) + var st = autoplay.run_level(db, SEED_BASE + i, 1, panel.atk_pct(), panel.hp_pct(), false, logger) + (stats["dmg_all"] as Array).append(st.hero_dmg) + (stats["rounds_all"] as Array).append(st.rounds) + if st.result == "victory": + stats["wins"] = int(stats["wins"]) + 1 + (stats["hp_left_win"] as Array).append(st.hp_left) + (stats["hp_boss"] as Array).append(st.hp_at_boss) + elif st.result == "defeat": + var key := "%d.%s" % [st.defeat_node_index, st.defeat_node_type] + stats["death_nodes"][key] = int(stats["death_nodes"].get(key, 0)) + 1 + + +func _init() -> void: + var frame_idx := {"i": -1} # 帧计数(宿主最先连——拓扑钉死「计数先于步进体」) + process_frame.connect(func() -> void: frame_idx["i"] += 1) + await process_frame # 树初始化就绪帧(SceneTree._init 时点 root 未挂树——面板 get_tree() 首帧后才有效;wd8 同款先例:panel 段前有 await 过帧) + var r3 := await _run_pair(3, frame_idx) + _check_n3(r3) + var r0 := await _run_pair(0, frame_idx) + _check_n0(r0) + quit(Kit.finish("tune_sim_batch_parity")) + + +# 对拍驱动:两形态各经 run(n) 真实入口(同参假面板/假驱动各自实例);先启动两形态、后分别 await—— +# 两形态在调用点同一同步段内先后挂起(并行于同一帧时钟),驱动协程返回 int 承接 void run 的可等待面 +func _run_pair(n: int, frame_idx: Dictionary) -> Dictionary: + # 两形态串行驱动(各经 run(n) 真实入口 await 调用——GDScript 协程动态调用必须 await, + # 无并行启动形态):帧序列各自记录、断言按相对首帧偏移归一后逐位比对——拓扑不变式 + # 「计数先连、启动后于计数、计数先于步进体」在两形态下相对次序同构,等价证据结构不变。 + var panel_a := FakePanel.new() + root.add_child(panel_a) + var drv_a := FakeDriver.new() + var a: Variant = TuneSimScript.new(FakeDB.new(), panel_a, 100, drv_a) + drv_a.ctrl = a + drv_a.frame_counter = func() -> int: return frame_idx["i"] + await a.run(n) + var panel_b := FakePanel.new() + root.add_child(panel_b) + var drv_b := FakeDriver.new() + var b: Variant = AwaitLoopRunner.new(FakeDB.new(), panel_b, 100, drv_b) + drv_b.ctrl = b + drv_b.frame_counter = func() -> int: return frame_idx["i"] + await b.run(n) + return {"a": a, "b": b, "pa": panel_a, "pb": panel_b, "da": drv_a, "db": drv_b} + + +func _check_n3(r: Dictionary) -> void: + var pa: FakePanel = r["pa"] + var pb: FakePanel = r["pb"] + var da: FakeDriver = r["da"] + var drv_b: FakeDriver = r["db"] + var a = r["a"] + var b = r["b"] + Kit.check("n=3 帧序列相对偏移逐位相等(%s vs %s)+相邻差恒一(每步恰一帧)〔测速 V1〕", + str(_norm(da.frames)) == str(_norm(drv_b.frames)) and da.frames.size() == 3 + and _consecutive(da.frames) and _consecutive(drv_b.frames)) + Kit.check("n=3 运行态读数序列逐位相等+收尾双运行态假+面板置假序列同构〔测速 V2〕", + str(da.seen) == str(drv_b.seen) and da.seen == [true, true, true] + and not a.is_running() and not b.is_running() + and not pa.running and not pb.running + and str(pa.running_log) == str(pb.running_log)) + Kit.check("n=3 进度串序列逐位相等(%d 段)〔测速 V3〕", + str(pa.progress_log) == str(pb.progress_log) and pa.progress_log.size() == 4) + var got: Dictionary = r["b"].out_stats + var deaths: Dictionary = got.get("death_nodes", {}) + Kit.check("n=3 夹具 stats 六键与按假驱动固定返回值重算的期望逐键相等〔测速 V4〕", + int(got.get("wins", -1)) == 2 + and _arr_eq(got.get("dmg_all"), [12.0, 6.0, 12.0]) + and _arr_eq(got.get("hp_left_win"), [3.0, 3.0]) + and _arr_eq(got.get("hp_boss"), [5.0, 5.0]) + and _arr_eq(got.get("rounds_all"), [4, 2, 4]) + and deaths.size() == 1 and int(deaths.get("3.battle", -1)) == 1) + Kit.check("n=3 报告全文逐字相等且含渲染面四关键串〔测速 V5〕", + pa.report == pb.report and pa.report != "" + and pa.report.contains("关卡完成率") and pa.report.contains("关卡完成剩余 HP 均") + and pa.report.contains("败北节点") and pa.report.contains("N=3")) + + +func _check_n0(r: Dictionary) -> void: + var pa: FakePanel = r["pa"] + var pb: FakePanel = r["pb"] + var da: FakeDriver = r["da"] + var drv_b: FakeDriver = r["db"] + var a = r["a"] + var b = r["b"] + Kit.check("n=0 退化:两形态驱动零回调、帧索引序列空(同步收尾零帧)〔测速 V1〕", + da.frames.is_empty() and drv_b.frames.is_empty() and da.calls == 0 and drv_b.calls == 0) + Kit.check("n=0 运行态置假序列 [true, false] 双形态同构+收尾双假〔测速 V2〕", + str(pa.running_log) == str(pb.running_log) and pa.running_log == [true, false] + and not a.is_running() and not b.is_running()) + Kit.check("n=0 进度串单段逐字相等〔测速 V3〕", + str(pa.progress_log) == str(pb.progress_log) and pa.progress_log == ["模拟中:0/0 …"]) + Kit.check("n=0 夹具 stats 六键零计〔测速 V4〕", + int((r["b"].out_stats as Dictionary).get("wins", -1)) == 0 + and ((r["b"].out_stats as Dictionary).get("dmg_all") as Array).is_empty() + and ((r["b"].out_stats as Dictionary).get("hp_left_win") as Array).is_empty() + and ((r["b"].out_stats as Dictionary).get("hp_boss") as Array).is_empty() + and ((r["b"].out_stats as Dictionary).get("rounds_all") as Array).is_empty() + and ((r["b"].out_stats as Dictionary).get("death_nodes") as Dictionary).is_empty()) + Kit.check("n=0 报告全文逐字相等且非空(空 stats 渲染面)〔测速 V5〕", + pa.report == pb.report and pa.report != "") + +# 帧序列归一(相对首帧偏移——串行驱动下两形态帧段错位、结构可比性归一承载) +func _norm(fr: Array) -> Array: + var out: Array = [] + var base := int(fr[0]) if fr.size() > 0 else 0 + for i in fr.size(): + out.append(int(fr[i]) - base) + return out + + +# 相邻差恒一(每步恰占一帧——判据①的序列面证据) +func _consecutive(fr: Array) -> bool: + for i in range(1, fr.size()): + if int(fr[i]) - int(fr[i - 1]) != 1: + return false + return true + +# 数组逐元素浮点比对(stats 桶重算期望——类型化比较,消 str 形态歧义) +func _arr_eq(a, b: Array) -> bool: + var aa := a as Array + if aa == null or aa.size() != b.size(): + return false + for i in b.size(): + if absf(float(aa[i]) - float(b[i])) > 0.0001: + return false + return true