Files
gplan-2/game/tests/unit/presentation/tune_sim_batch_parity_test.gd

246 lines
11 KiB
GDScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 双实现对拍配对套件(注册面批同代测速循环批 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