T6 探针基建收口段: 三族样板收单点(probe_ui_util树寻址contains/begins_with参数化+isolate_save七份隔离样板/battle_driver五份战斗驱动)+level_autoplay双战斗循环合并_decide_act+test_kit收13套件_check样板(断言数逐套件零丢失对照表在报告)+BAD_DIR夹具共享+smoke_all五件(:953公有面clamped_pool/_battle_st七袋/check_silent并入quiet参/_pct转发删/load改preload);探针小件=main句柄Node类型注/魔法等待常量化/画布常数同源锚注/死参verbose删;红线守住=ui_input case④b与click循环体零触碰/拍板点三件未退役
This commit is contained in:
1 parent
0691a188fd
commit
315e46038c
32 files changed
+587
-764
No files matched your search
@@ -0,0 +1,37 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# 战斗驱动循环单点(T6 样板收口批 2026-10-02)——smoke 族探针共用的「挖掘至停手」整轮驱动。
|
||||
# 策略 = AutoPlayer.choose_dig(O4 单一实现):挖最近可挖格(异格先移位)/ 无可挖或相位离 Active 停手。
|
||||
# 语义锚(monte_carlo 原型逐位保持):guard 400 步防挂死;移位后相位/轮次离场即 break;
|
||||
# 轮末仍 Active 且未换轮 → settle_batch stop。smoke_all._run_battle/_auto_play_round 为冒烟本体
|
||||
# 不经本面(含回归断言——2026-10-02 修复批加,禁并入);fx_shot_probe 为带墙钟抓帧的异步变体亦不在列。
|
||||
extends RefCounted
|
||||
|
||||
const AutoPlayer := preload("res://smoke/auto_player.gd")
|
||||
|
||||
const GUARD_ACTIONS := 400 # 单轮动作步防挂死上限(被收口探针原值逐一同值)
|
||||
|
||||
|
||||
# 驱动一整轮挖掘。still_running = 调用方战斗活性谓词(缺省恒真):
|
||||
# monte_carlo / skill_rhythm 传 `func(): return out.result == ""`(battle_finished 已发的轮内不再动手),
|
||||
# damage_probe 关卡驱动传 runner 绑定 + battle_result 谓词,battle 级探针传空即可。
|
||||
static func drive_round(settlement, still_running: Callable = Callable()) -> void:
|
||||
var start_round: int = settlement.round_index
|
||||
var minesweeper = settlement.minesweeper
|
||||
var guard := GUARD_ACTIONS
|
||||
while _alive(still_running) and settlement.action_economy.phase == "Active" \
|
||||
and settlement.round_index == start_round and guard > 0:
|
||||
guard -= 1
|
||||
var plan := AutoPlayer.choose_dig(minesweeper)
|
||||
if plan.is_empty():
|
||||
break
|
||||
if plan.hero_cell != minesweeper.hero_pos():
|
||||
minesweeper.settle_batch([{"action": "move", "cell": plan.hero_cell}])
|
||||
if settlement.action_economy.phase != "Active" or settlement.round_index != start_round:
|
||||
break
|
||||
minesweeper.settle_batch([{"action": "dig", "cell": plan.dig_cell}])
|
||||
if _alive(still_running) and settlement.action_economy.phase == "Active" and settlement.round_index == start_round:
|
||||
minesweeper.settle_batch([{"action": "stop"}])
|
||||
|
||||
|
||||
static func _alive(still_running: Callable) -> bool:
|
||||
return still_running.is_null() or bool(still_running.call())
|
||||
@@ -0,0 +1 @@
|
||||
uid://dbattledrv13r
|
||||
@@ -12,7 +12,7 @@
|
||||
# Godot_console.exe --headless --path . --script res://smoke/damage_probe.gd -- level 42
|
||||
extends SceneTree
|
||||
|
||||
const AutoPlayer := preload("res://smoke/auto_player.gd")
|
||||
const BattleDriver := preload("res://smoke/battle_driver.gd") # T6 收口:挖掘驱动循环单点(策略仍归 AutoPlayer 单一实现)
|
||||
|
||||
var _checks := 0
|
||||
var _fails := 0
|
||||
@@ -67,7 +67,7 @@ func _probe_boss(db, seed_val: int) -> void:
|
||||
|
||||
print("--- Run A:fresh 池(零挖掘停手轮——无授予,攻击链裸验)---")
|
||||
var run_a := _run_boss(db, seed_val, false)
|
||||
_print_rounds(db, math, run_a, true)
|
||||
_print_rounds(db, math, run_a)
|
||||
var a_ok := _assert_rounds(db, math, run_a, true)
|
||||
check(a_ok.fresh_and_full, "A1 攻击链健康(fresh 池·掷点口径):非蓄力拍 d_tick = max(下限, 掷点档位基线+附加)(×%d 拍)——H2 攻击链缺陷否证" % a_ok.n_attack)
|
||||
check(a_ok.arith_ok, "A2 池算术完整:全部非蓄力拍 d_tick == 掷点口径期望(×%d 拍)" % a_ok.n_attack)
|
||||
@@ -77,7 +77,7 @@ func _probe_boss(db, seed_val: int) -> void:
|
||||
|
||||
print("--- Run B:自动挖掘(授予照常发生——当前数据承伤行为实测)---")
|
||||
var run_b := _run_boss(db, seed_val, true)
|
||||
_print_rounds(db, math, run_b, true)
|
||||
_print_rounds(db, math, run_b)
|
||||
var b_ok := _assert_rounds(db, math, run_b, false)
|
||||
check(b_ok.arith_ok, "B1 池算术完整(含授予轮):全部非蓄力拍 d_tick == 掷点口径期望(×%d 拍)" % b_ok.n_attack)
|
||||
check(b_ok.wall_tick_positive, "B2 负池轮不归零(伤害下限):hero_def<0 的轮 d_tick 恒 ≥ damage_floor(×%d 轮,最深实测池 %d)" % [b_ok.n_wall, b_ok.deepest])
|
||||
@@ -98,28 +98,16 @@ func _run_boss(db, seed_val: int, dig: bool) -> Dictionary:
|
||||
var rounds := 0
|
||||
while settlement.battle_result == "" and rounds < 40:
|
||||
rounds += 1
|
||||
var start_round: int = settlement.round_index
|
||||
if dig:
|
||||
var minesweeper = settlement.minesweeper
|
||||
var guard := 400
|
||||
while settlement.action_economy.phase == "Active" and settlement.round_index == start_round and guard > 0:
|
||||
guard -= 1
|
||||
var plan := AutoPlayer.choose_dig(minesweeper)
|
||||
if plan.is_empty():
|
||||
break
|
||||
if plan.hero_cell != minesweeper.hero_pos():
|
||||
minesweeper.settle_batch([{"action": "move", "cell": plan.hero_cell}])
|
||||
if settlement.action_economy.phase == "Active":
|
||||
minesweeper.settle_batch([{"action": "dig", "cell": plan.dig_cell}])
|
||||
if settlement.action_economy.phase == "Active" and settlement.round_index == start_round:
|
||||
minesweeper.settle_batch([{"action": "stop"}])
|
||||
BattleDriver.drive_round(settlement)
|
||||
else:
|
||||
settlement.minesweeper.settle_batch([{"action": "stop"}])
|
||||
return cap
|
||||
|
||||
|
||||
# 逐轮:授予行汇总 → 重建战斗段时点池值(effect_pool_map 表驱动归属)→ 敌行动行逐行打印
|
||||
func _print_rounds(db, math, run: Dictionary, verbose: bool) -> void:
|
||||
#(T6 收口批:原形参 verbose 为死参——打印行为无档位差异,随批删除)
|
||||
func _print_rounds(db, math, run: Dictionary) -> void:
|
||||
var groups := _round_groups(run.rows)
|
||||
var beats := _played_beats(run, db)
|
||||
var rounds: Array = groups.keys()
|
||||
@@ -356,22 +344,8 @@ func _drive_battle(runner, settlement, cap: Dictionary) -> void:
|
||||
var round_guard := 60
|
||||
while cap["result"] == "" and runner.current_battle() == settlement and settlement.battle_result == "" and round_guard > 0:
|
||||
round_guard -= 1
|
||||
var start_round: int = settlement.round_index
|
||||
var minesweeper = settlement.minesweeper
|
||||
var action_guard := 400
|
||||
while runner.current_battle() == settlement and settlement.battle_result == "" and settlement.action_economy.phase == "Active" \
|
||||
and settlement.round_index == start_round and action_guard > 0:
|
||||
action_guard -= 1
|
||||
var plan := AutoPlayer.choose_dig(minesweeper)
|
||||
if plan.is_empty():
|
||||
break
|
||||
if plan.hero_cell != minesweeper.hero_pos():
|
||||
minesweeper.settle_batch([{"action": "move", "cell": plan.hero_cell}])
|
||||
if settlement.action_economy.phase != "Active" or settlement.round_index != start_round:
|
||||
break
|
||||
minesweeper.settle_batch([{"action": "dig", "cell": plan.dig_cell}])
|
||||
if runner.current_battle() == settlement and settlement.battle_result == "" and settlement.action_economy.phase == "Active" and settlement.round_index == start_round:
|
||||
minesweeper.settle_batch([{"action": "stop"}])
|
||||
BattleDriver.drive_round(settlement, func() -> bool:
|
||||
return runner.current_battle() == settlement and settlement.battle_result == "")
|
||||
|
||||
|
||||
# 战斗判定摘要:round-1 victory 的战斗必须能从结算行分辨「首轮全灭」vs「空场」——
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
# 运行:Godot_console.exe --headless --path . --script res://smoke/dump_battle.gd
|
||||
extends SceneTree
|
||||
|
||||
const AutoPlayer := preload("res://smoke/auto_player.gd") # O4 收口:自动玩家策略单一实现
|
||||
const BattleDriver := preload("res://smoke/battle_driver.gd") # T6 收口:挖掘驱动循环单点(策略仍归 AutoPlayer 单一实现)
|
||||
|
||||
func _initialize() -> void:
|
||||
var db = load("res://core/config_db.gd").new()
|
||||
@@ -28,21 +28,7 @@ func _initialize() -> void:
|
||||
var rounds := 0
|
||||
while settlement.battle_result == "" and rounds < 40:
|
||||
rounds += 1
|
||||
var start_round: int = settlement.round_index
|
||||
var minesweeper = settlement.minesweeper
|
||||
var action_economy = settlement.action_economy
|
||||
var guard := 400
|
||||
while action_economy.phase == "Active" and settlement.round_index == start_round and guard > 0:
|
||||
guard -= 1
|
||||
var plan := AutoPlayer.choose_dig(minesweeper)
|
||||
if plan.is_empty():
|
||||
break
|
||||
if plan.hero_cell != minesweeper.hero_pos():
|
||||
minesweeper.settle_batch([{"action": "move", "cell": plan.hero_cell}])
|
||||
if action_economy.phase == "Active":
|
||||
minesweeper.settle_batch([{"action": "dig", "cell": plan.dig_cell}])
|
||||
if action_economy.phase == "Active" and settlement.round_index == start_round:
|
||||
minesweeper.settle_batch([{"action": "stop"}])
|
||||
BattleDriver.drive_round(settlement) # T6 收口:原内联循环语义同型(移动期免 AP——移动不换轮,移位后重查 break 分支为防御性等价)
|
||||
print("战斗结果: %s(%d 轮)" % [settlement.battle_result, rounds])
|
||||
print("=== 结算步骤流(%d 行)===" % stream.size())
|
||||
for row in stream:
|
||||
|
||||
+16
-24
@@ -6,7 +6,16 @@
|
||||
# 渲染在(本探针 PNG)」双证的第二半边。PNG 落输出目录(artwork/qa-fx-*/,素材纪律不入 git)。
|
||||
extends Node
|
||||
|
||||
var main
|
||||
const ProbeUiUtil := preload("res://smoke/probe_ui_util.gd") # T6 收口:树寻址/隔离样板单点
|
||||
|
||||
# ── 演出采样窗口口径(拍板点:headless/窗口化帧率不定,帧时刻以墙钟近似锚定;改窗口 = 改抓帧
|
||||
# 时刻观测面,非断言语义。窗口值与原字面量逐一同值——T6 收口批仅提具名。) ──
|
||||
const T_DIG_FLIGHT := 0.12 # 挖格行到达/弹道在飞窗口(settle_batch 回执后停留)
|
||||
const T_SETTLE := 0.35 # 结算排播窗口(行到达后二连抓)
|
||||
const T_ROUND_TICK := 0.25 # 停手轮末(敌方攻击弹道/受击闪白 0.2-1.6s 流过)连抓 6 帧的采样步距
|
||||
const T_FINAL := 0.6 # 收尾帧前静置
|
||||
|
||||
var main: Node
|
||||
var out_dir := ""
|
||||
var _problems: Array = []
|
||||
|
||||
@@ -19,17 +28,10 @@ func _ready() -> void:
|
||||
main = load("res://presentation/main.tscn").instantiate()
|
||||
add_child(main)
|
||||
await get_tree().process_frame
|
||||
for p0 in ["user://fx_shot_probe.json", "user://fx_shot_probe.json.tmp", "user://fx_shot_probe_settings.cfg"]:
|
||||
if FileAccess.file_exists(p0):
|
||||
DirAccess.remove_absolute(p0)
|
||||
var iso_c = load("res://core/save_manager.gd").new("user://fx_shot_probe.json")
|
||||
iso_c.load_from_disk()
|
||||
main.save_manager = iso_c
|
||||
main.meta = load("res://core/meta_progression.gd").new(main.db, iso_c)
|
||||
main.settings_store = load("res://presentation/settings_store.gd").new("user://fx_shot_probe_settings.cfg")
|
||||
ProbeUiUtil.isolate_save(main, "fx_shot_probe", false, true, false)
|
||||
for i in 8:
|
||||
await get_tree().process_frame # UI 装配完成等足帧(canvas_item 空引用窗口)
|
||||
var lvbtn: Button = _find_button_by_sub(main._start_overlay, "开始冒险")
|
||||
var lvbtn: Button = ProbeUiUtil.find_button(main._start_overlay, "开始冒险", "contains")
|
||||
if lvbtn == null:
|
||||
_fail("开始冒险按钮缺失")
|
||||
return
|
||||
@@ -68,9 +70,9 @@ func _ready() -> void:
|
||||
if out.is_empty() or not bool(out[0].get("accepted", false)):
|
||||
continue
|
||||
dug += 1
|
||||
await get_tree().create_timer(0.12).timeout
|
||||
await get_tree().create_timer(T_DIG_FLIGHT).timeout
|
||||
await _snap("fx_dig%d" % dug) # 行到达/弹道在飞窗口
|
||||
await get_tree().create_timer(0.35).timeout
|
||||
await get_tree().create_timer(T_SETTLE).timeout
|
||||
await _snap("fx_settle%d" % dug) # 结算排播窗口
|
||||
continue
|
||||
# 无相邻可挖 → 走向有未开安全格邻居的已揭开格(挖掘期移动免费)
|
||||
@@ -90,12 +92,12 @@ func _ready() -> void:
|
||||
# 停手 → 轮末结算(敌方攻击弹道/受击闪白/结算排播在 0.2-1.6s 窗口流过)——连抓 6 帧
|
||||
rules.settle_batch([{"action": "stop"}])
|
||||
for i in 6:
|
||||
await get_tree().create_timer(0.25).timeout
|
||||
await get_tree().create_timer(T_ROUND_TICK).timeout
|
||||
await _snap("fx_round%d" % (i + 1))
|
||||
if dug == 0:
|
||||
_fail("无安全格可挖(首板全雷不可信)")
|
||||
return
|
||||
await get_tree().create_timer(0.6).timeout
|
||||
await get_tree().create_timer(T_FINAL).timeout
|
||||
await _snap("fx_final")
|
||||
if _problems.is_empty():
|
||||
print("[fx_shot_probe] PASS — 演出帧 %d 张落 %s(挖格 %d)" % [dug * 2 + 1, out_dir, dug])
|
||||
@@ -115,16 +117,6 @@ func _snap(tag: String) -> void:
|
||||
img.save_png("%s/%s.png" % [out_dir, tag])
|
||||
|
||||
|
||||
func _find_button_by_sub(root: Node, needle: String) -> Button:
|
||||
if root is Button and String(root.text).contains(needle):
|
||||
return root
|
||||
for c in root.get_children():
|
||||
var r := _find_button_by_sub(c, needle)
|
||||
if r != null:
|
||||
return r
|
||||
return null
|
||||
|
||||
|
||||
func _fail(msg: String) -> void:
|
||||
_problems.append(msg)
|
||||
push_error("[fx_shot_probe] FAIL — " + msg)
|
||||
|
||||
@@ -396,13 +396,17 @@ static func _on_level_finished(p: Dictionary, st: Dictionary, logger) -> void:
|
||||
|
||||
|
||||
static func _autoplay_battle(runner, settlement, st: Dictionary, logger) -> void:
|
||||
if String(st.get("policy", "normal")) == "perfect":
|
||||
_autoplay_battle_perfect(runner, settlement, st, logger)
|
||||
return
|
||||
# 策略档经 _decide_act 单点分派(T6 样板收口批 2026-10-02——原 _autoplay_battle/_autoplay_battle_perfect
|
||||
# 双循环逐行相同仅差 act 来源行,合并单循环;三档决策语义逐位不变,见 _decide_act)。
|
||||
# 普通策略档 v3(2026-10-01 用户重定义):完美策略 + 受控失误注入——v2「新手级单步推理+盲挖
|
||||
# 兜底」画像被真人实证推翻(零加成真人首通三关踩 23 雷 vs v2 普通档 L2 条件通关率 10.6%)。
|
||||
# v3 语义 = 会玩的普通玩家:推理与完美档同源(subset 解算/标旗/卡死刷新),失误只注入两手——
|
||||
# 标雷误挖(NORMAL_MINE_SLIP_P)+ 安全挖跳过(NORMAL_SKIP_DIG_P)。连续拒绝 ≥3 = 编排违约防御。
|
||||
# 完美策略档(2026-09-14):单步决策循环——move/dig/flag 经 排雷玩法 settle_batch 提交,
|
||||
# refresh 经 settlement.player_request_refresh(战斗结算 核心编排:行动经济 扣 RAP + 退役 + 当场换新板),
|
||||
# stop 出内环走统一停手提交。连续拒绝 ≥3 = 编排违约防御(fail-fast 语义,停手防死循环)。
|
||||
# 2026-10-01 上界修正:选路改目标价值评分(_route_pick)+ 商店贪心买齐(_autoplay_shop)——
|
||||
# 「完美」须是元游戏意义上的上界(会购物会补给),不再只是扫雷子游戏上界。
|
||||
var round_guard := 60
|
||||
while st.result == "" and runner.current_battle() == settlement and settlement.battle_result == "" and round_guard > 0:
|
||||
round_guard -= 1
|
||||
@@ -413,11 +417,7 @@ static func _autoplay_battle(runner, settlement, st: Dictionary, logger) -> void
|
||||
while runner.current_battle() == settlement and settlement.battle_result == "" and settlement.action_economy.phase == "Active" \
|
||||
and settlement.round_index == start_round and action_guard > 0 and rejects < 3:
|
||||
action_guard -= 1
|
||||
var act: Dictionary
|
||||
if bool(st.get("adv", false)):
|
||||
act = _apply_normal_slips(AutoPlayer.perfect_action(settlement.minesweeper, settlement), settlement, st) # v3:完美+失误
|
||||
else:
|
||||
act = AutoPlayer.novice_action(settlement.minesweeper) # 单关机制夹具:v2 新手画像保持
|
||||
var act: Dictionary = _decide_act(st, settlement)
|
||||
var kind := String(act.get("kind", "stop"))
|
||||
if kind == "stop":
|
||||
break
|
||||
@@ -487,47 +487,16 @@ static func _gamble_action(plan: Dictionary, settlement) -> Dictionary:
|
||||
return {"kind": "move", "cell": plan.hero_cell}
|
||||
|
||||
|
||||
# 完美策略档战斗驱动(2026-09-14):单步决策循环——move/dig/flag 经 排雷玩法 settle_batch 提交,
|
||||
# refresh 经 settlement.player_request_refresh(战斗结算 核心编排:行动经济 扣 RAP + 退役 + 当场换新板),
|
||||
# stop 出内环走统一停手提交。连续拒绝 ≥3 = 编排违约防御(fail-fast 语义,停手防死循环)。
|
||||
# 2026-10-01 上界修正:选路改目标价值评分(_route_pick)+ 商店贪心买齐(_autoplay_shop)——
|
||||
# 「完美」须是元游戏意义上的上界(会购物会补给),不再只是扫雷子游戏上界。
|
||||
static func _autoplay_battle_perfect(runner, settlement, st: Dictionary, logger) -> void:
|
||||
var round_guard := 60
|
||||
while st.result == "" and runner.current_battle() == settlement and settlement.battle_result == "" and round_guard > 0:
|
||||
round_guard -= 1
|
||||
st.rounds += 1
|
||||
var start_round: int = settlement.round_index
|
||||
var action_guard := 400
|
||||
var rejects := 0
|
||||
while runner.current_battle() == settlement and settlement.battle_result == "" and settlement.action_economy.phase == "Active" \
|
||||
and settlement.round_index == start_round and action_guard > 0 and rejects < 3:
|
||||
action_guard -= 1
|
||||
var act := AutoPlayer.perfect_action(settlement.minesweeper, settlement)
|
||||
var kind := String(act.get("kind", "stop"))
|
||||
if kind == "stop":
|
||||
break
|
||||
var ok := false
|
||||
if kind == "move":
|
||||
ok = _submit_intent(settlement, "move", act.get("cell"), logger, start_round)
|
||||
elif kind == "dig":
|
||||
ok = _submit_intent(settlement, "dig", act.get("cell"), logger, start_round)
|
||||
elif kind == "flag":
|
||||
ok = _submit_intent(settlement, "flag_place", act.get("cell"), logger, start_round)
|
||||
elif kind == "refresh":
|
||||
ok = bool(settlement.player_request_refresh())
|
||||
if logger != null:
|
||||
logger.log_action("refresh", Vector2i(-1, -1), ok, {"round": start_round})
|
||||
if ok:
|
||||
if kind == "refresh":
|
||||
st.refreshes = int(st.refreshes) + 1
|
||||
rejects = 0
|
||||
else:
|
||||
rejects += 1
|
||||
if runner.current_battle() == settlement and settlement.battle_result == "" and settlement.action_economy.phase == "Active" and settlement.round_index == start_round:
|
||||
var out_s: Array = settlement.minesweeper.settle_batch([{"action": "stop"}])
|
||||
if logger != null:
|
||||
logger.log_action("stop", Vector2i(-1, -1), _accepted(out_s), {"round": start_round})
|
||||
# 战斗决策单行分派(T6 双循环合并抽取——原两循环唯一差异行):
|
||||
# perfect 档 = 完美策略单步决策(上界口径);
|
||||
# normal 档 v3 = 完美+受控失误注入(冒险测量面 adv=true);单关机制夹具保持 v2 新手画像(adv 缺省)。
|
||||
# 决策函数纯读板面/编排面(零 RNG 消费——失误流独走 st.prng,见 _apply_normal_slips)。
|
||||
static func _decide_act(st: Dictionary, settlement) -> Dictionary:
|
||||
if String(st.get("policy", "normal")) == "perfect":
|
||||
return AutoPlayer.perfect_action(settlement.minesweeper, settlement)
|
||||
if bool(st.get("adv", false)):
|
||||
return _apply_normal_slips(AutoPlayer.perfect_action(settlement.minesweeper, settlement), settlement, st) # v3:完美+失误
|
||||
return AutoPlayer.novice_action(settlement.minesweeper) # 单关机制夹具:v2 新手画像保持
|
||||
|
||||
|
||||
static func _submit_intent(settlement, action: String, cell, logger, round_index: int) -> bool:
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
# 运行:Godot_console.exe --headless --path . --script res://smoke/monte_carlo.gd
|
||||
extends SceneTree
|
||||
|
||||
const AutoPlayer := preload("res://smoke/auto_player.gd") # O4 收口(批注 2026-09-15:原文件缺失 preload——HEAD 起即解析失败,本批顺手修复)
|
||||
const BattleDriver := preload("res://smoke/battle_driver.gd") # T6 收口:挖掘驱动循环单点(策略仍归 AutoPlayer 单一实现)
|
||||
|
||||
const N := 30
|
||||
const SEED0 := 42
|
||||
@@ -43,7 +43,7 @@ func _run_encounter(db, L: int) -> void:
|
||||
else:
|
||||
timeouts += 1
|
||||
print("=== L%d · %d 种子(败 %d / 超时 %d)===" % [L, N, losses, timeouts])
|
||||
print(" 胜利轮数 均值 %.2f 中位 %d p10 %.1f p90 %.1f 分布 %s" % [_mean(rounds_v), _median(rounds_v), _pct(rounds_v, 0.1), _pct(rounds_v, 0.9), _hist(rounds_v)])
|
||||
print(" 胜利轮数 均值 %.2f 中位 %d p10 %.1f p90 %.1f 分布 %s" % [_mean(rounds_v), _median(rounds_v), _pct_f(rounds_v, 0.1), _pct_f(rounds_v, 0.9), _hist(rounds_v)])
|
||||
print(" 每轮承伤 均值 %.2f 中位 %.1f p90 %.1f" % [_mean(dmg_per_round), _median_f(dmg_per_round), _pct_f(dmg_per_round, 0.9)])
|
||||
print(" 整场承伤 均值 %.1f 中位 %.0f(主角 max_hp=%d)" % [_mean(dmg_total), _median_f(dmg_total), int(db.hero.max_hp)])
|
||||
print(" 开安全格 均值 %.1f 中位 %d" % [_mean(safe_opened), _median(safe_opened)])
|
||||
@@ -72,21 +72,7 @@ func _run_battle(db, seed_val: int, L: int) -> Dictionary:
|
||||
while out.result == "" and guard_rounds > 0:
|
||||
guard_rounds -= 1
|
||||
out.rounds += 1
|
||||
var start_round: int = settlement.round_index
|
||||
var minesweeper = settlement.minesweeper
|
||||
var guard := 400
|
||||
while settlement.action_economy.phase == "Active" and settlement.round_index == start_round and guard > 0 and out.result == "":
|
||||
guard -= 1
|
||||
var plan := AutoPlayer.choose_dig(minesweeper)
|
||||
if plan.is_empty():
|
||||
break
|
||||
if plan.hero_cell != minesweeper.hero_pos():
|
||||
minesweeper.settle_batch([{"action": "move", "cell": plan.hero_cell}])
|
||||
if settlement.action_economy.phase != "Active" or settlement.round_index != start_round:
|
||||
break
|
||||
minesweeper.settle_batch([{"action": "dig", "cell": plan.dig_cell}])
|
||||
if settlement.action_economy.phase == "Active" and settlement.round_index == start_round and out.result == "":
|
||||
minesweeper.settle_batch([{"action": "stop"}])
|
||||
BattleDriver.drive_round(settlement, func() -> bool: return out.result == "")
|
||||
if out.result == "":
|
||||
out.result = "timeout"
|
||||
return out
|
||||
@@ -114,10 +100,6 @@ func _median_f(a: Array) -> float:
|
||||
return float(b[b.size() / 2])
|
||||
|
||||
|
||||
func _pct(a: Array, p: float) -> float:
|
||||
return _pct_f(a, p)
|
||||
|
||||
|
||||
func _pct_f(a: Array, p: float) -> float:
|
||||
if a.is_empty():
|
||||
return 0.0
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# 探针共用面:树寻址助手 + 存档/设置/元进度隔离样板单点(T6 样板收口批 2026-10-02)。
|
||||
# 收口范围:ui_probe / ui_click_probe / shot_probe / fx_shot_probe 四探针的 7 处树寻址样板
|
||||
# 与 7 处隔离样板(各探针隔离文件名后缀 tag 保持不变)。
|
||||
# 用法:探针侧 `const ProbeUiUtil := preload("res://smoke/probe_ui_util.gd")` 后静态调用
|
||||
#(探针载体含 --script 场景两种,均无全局标识符——禁 class_name 裸引用)。
|
||||
extends RefCounted
|
||||
|
||||
const SaveManagerScript := preload("res://core/save_manager.gd")
|
||||
const MetaProgressionScript := preload("res://core/meta_progression.gd")
|
||||
const SettingsStoreScript := preload("res://presentation/settings_store.gd")
|
||||
|
||||
# ── 树寻址(递归只读——按钮/标签句柄未存组合根成员的探针专用面) ──────────────
|
||||
# 匹配模式参数化(历史两种语义,禁混用禁静默改变):
|
||||
# mode="prefix" = text.begins_with(needle)(ui_probe / ui_click_probe 历史语义)
|
||||
# mode="contains" = text.contains(needle)(shot_probe / fx_shot_probe 历史语义)
|
||||
# 调用点替换时逐一核对原语义后选参。
|
||||
|
||||
static func find_button(root: Node, needle: String, mode: String = "prefix") -> Button:
|
||||
if root == null:
|
||||
return null
|
||||
if root is Button and _text_match(String(root.text), needle, mode):
|
||||
return root
|
||||
for child in root.get_children():
|
||||
var found := find_button(child, needle, mode)
|
||||
if found != null:
|
||||
return found
|
||||
return null
|
||||
|
||||
|
||||
# CheckButton 同款寻址(ui_click_probe 历史语义 = contains——设置面旧入口退役断言用)
|
||||
static func find_check_button(root: Node, needle: String) -> CheckButton:
|
||||
if root == null:
|
||||
return null
|
||||
if root is CheckButton and String(root.text).contains(needle):
|
||||
return root
|
||||
for child in root.get_children():
|
||||
var found := find_check_button(child, needle)
|
||||
if found != null:
|
||||
return found
|
||||
return null
|
||||
|
||||
|
||||
# Label 按文案包含寻址(shot_probe GP 徽章读数锚)
|
||||
static func find_label(root: Node, text_fragment: String) -> Label:
|
||||
if root == null:
|
||||
return null
|
||||
if root is Label and String(root.text).contains(text_fragment):
|
||||
return root
|
||||
for child in root.get_children():
|
||||
var found := find_label(child, text_fragment)
|
||||
if found != null:
|
||||
return found
|
||||
return null
|
||||
|
||||
|
||||
# 控件实测几何(shot_probe checker 文字框取样坐标源——禁硬编码像素,分辨率档位自适应)
|
||||
static func label_rect(root: Node, text_fragment: String) -> Dictionary:
|
||||
if root == null:
|
||||
return {}
|
||||
if root is Label and String(root.text).contains(text_fragment):
|
||||
return {"x": root.global_position.x, "y": root.global_position.y,
|
||||
"w": root.size.x, "h": root.size.y}
|
||||
for child in root.get_children():
|
||||
var r := label_rect(child, text_fragment)
|
||||
if not r.is_empty():
|
||||
return r
|
||||
return {}
|
||||
|
||||
|
||||
# 按脚本名寻址(resource_path contains——shot_probe hero 面板锚)
|
||||
static func find_by_script(root: Node, script_name: String) -> Node:
|
||||
if root == null:
|
||||
return null
|
||||
var s: Script = root.get_script()
|
||||
if s != null and String(s.resource_path).contains(script_name):
|
||||
return root
|
||||
for child in root.get_children():
|
||||
var r := find_by_script(child, script_name)
|
||||
if r != null:
|
||||
return r
|
||||
return null
|
||||
|
||||
|
||||
static func _text_match(text: String, needle: String, mode: String) -> bool:
|
||||
return text.begins_with(needle) if mode == "prefix" else text.contains(needle)
|
||||
|
||||
|
||||
# ── 存档/设置/元进度三面隔离(探针禁碰真实档 user://save.json——SaveManager 路径注入纪律) ──
|
||||
# tag = 隔离档名(不带扩展名,如 "smoke_input_probe_save"——各探针后缀保持不变);先删后建防陈旧档泄入。
|
||||
# mark_first_done = 对齐真实档游玩史口径(模板抽签路径不变);with_settings = 连设置面换注
|
||||
#(隔离 cfg 名 = <tag>_settings.cfg,与被收口探针原文件名逐一同名);refresh_resume = 组合根
|
||||
# 「继续上局」按钮重刷;swap_save = 是否换注 main.save_manager(营地先行段只换 meta 不换 save_manager)。
|
||||
# 返回隔离 SaveManager 实例(调用方常需后续直读余额/重读档)。
|
||||
static func isolate_save(main, tag: String, mark_first_done: bool = false, with_settings: bool = false,
|
||||
refresh_resume: bool = false, swap_save: bool = true):
|
||||
var save_path := "user://%s.json" % tag
|
||||
for p in [save_path, save_path + ".tmp", "user://%s_settings.cfg" % tag]:
|
||||
if FileAccess.file_exists(p):
|
||||
DirAccess.remove_absolute(p)
|
||||
var iso = SaveManagerScript.new(save_path)
|
||||
iso.load_from_disk()
|
||||
if mark_first_done:
|
||||
iso.mark_first_adventure_done()
|
||||
if swap_save:
|
||||
main.save_manager = iso
|
||||
main.meta = MetaProgressionScript.new(main.db, iso)
|
||||
if with_settings:
|
||||
main.settings_store = SettingsStoreScript.new("user://%s_settings.cfg" % tag)
|
||||
if refresh_resume:
|
||||
main._refresh_resume_button()
|
||||
return iso
|
||||
@@ -0,0 +1 @@
|
||||
uid://c1probeuiutil
|
||||
+14
-71
@@ -9,7 +9,9 @@
|
||||
# 最小自检 = 四件存在且非零字节(像素方差断言归 tools/shot_checker.py——B 档骨架分工)。
|
||||
extends Node
|
||||
|
||||
var main
|
||||
const ProbeUiUtil := preload("res://smoke/probe_ui_util.gd") # T6 收口:树寻址/隔离样板单点
|
||||
|
||||
var main: Node
|
||||
var out_dir := ""
|
||||
var _problems: Array = []
|
||||
var _meta := {} # 控件实测几何(全局 rect——shot_checker 文字框/明度带取样坐标源,禁硬编码像素)
|
||||
@@ -23,36 +25,28 @@ func _ready() -> void:
|
||||
main = load("res://presentation/main.tscn").instantiate()
|
||||
add_child(main)
|
||||
await get_tree().process_frame
|
||||
# ── 存档/元进度/设置三面隔离(ui_click_probe 同款——真档零污染) ──
|
||||
for p0 in ["user://smoke_shot_probe.json", "user://smoke_shot_probe.json.tmp",
|
||||
"user://smoke_shot_probe_settings.cfg"]:
|
||||
if FileAccess.file_exists(p0):
|
||||
DirAccess.remove_absolute(p0)
|
||||
var iso_c = load("res://core/save_manager.gd").new("user://smoke_shot_probe.json")
|
||||
iso_c.load_from_disk()
|
||||
main.save_manager = iso_c
|
||||
main.meta = load("res://core/meta_progression.gd").new(main.db, iso_c)
|
||||
main.settings_store = load("res://presentation/settings_store.gd").new("user://smoke_shot_probe_settings.cfg")
|
||||
# ── 存档/元进度/设置三面隔离(ui_click_probe 同款——真档零污染;历史语义 = contains 寻址) ──
|
||||
ProbeUiUtil.isolate_save(main, "smoke_shot_probe", false, true, false)
|
||||
await get_tree().process_frame
|
||||
# ① 菜单(启动态)
|
||||
_meta["menu_sub"] = _find_label_rect(main._start_overlay, "改造批")
|
||||
_meta["menu_help"] = _find_label_rect(main._start_overlay, "玩法:")
|
||||
_meta["menu_title"] = _find_label_rect(main._start_overlay, "Dungeon Sweeper")
|
||||
_meta["menu_sub"] = ProbeUiUtil.label_rect(main._start_overlay, "改造批")
|
||||
_meta["menu_help"] = ProbeUiUtil.label_rect(main._start_overlay, "玩法:")
|
||||
_meta["menu_title"] = ProbeUiUtil.label_rect(main._start_overlay, "Dungeon Sweeper")
|
||||
await _snap("menu")
|
||||
# ② 营地
|
||||
var campbtn: Button = _find_button_by_prefix(main._start_overlay, "营地")
|
||||
var campbtn: Button = ProbeUiUtil.find_button(main._start_overlay, "营地", "contains")
|
||||
if campbtn == null:
|
||||
_problems.append("营地入口缺失(开始层 campbtn)")
|
||||
else:
|
||||
campbtn.pressed.emit()
|
||||
await get_tree().process_frame
|
||||
await _snap("camp")
|
||||
var close_btn: Button = _find_button_by_prefix(main.camp_overlay, "关闭营地")
|
||||
var close_btn: Button = ProbeUiUtil.find_button(main.camp_overlay, "关闭营地", "contains")
|
||||
if close_btn != null:
|
||||
close_btn.pressed.emit()
|
||||
await get_tree().process_frame
|
||||
# ③ 关卡画面(开始冒险→停点出图——冒险开始即出图,停点前移批语义)
|
||||
var lvbtn: Button = _find_button_by_prefix(main._start_overlay, "开始冒险")
|
||||
var lvbtn: Button = ProbeUiUtil.find_button(main._start_overlay, "开始冒险", "contains")
|
||||
if lvbtn == null:
|
||||
_problems.append("开始冒险按钮缺失(开始层 lvbtn)")
|
||||
else:
|
||||
@@ -61,7 +55,7 @@ func _ready() -> void:
|
||||
await get_tree().process_frame
|
||||
await _snap("level")
|
||||
# GP 常驻徽章断言(迭代 9 卡⑪核验——读数=runner.gp 现值,刷新=写路径回执后重查〔open_choices〕)
|
||||
var gp_label: Label = _find_label_by_text(main.level_screen, "◈ ")
|
||||
var gp_label: Label = ProbeUiUtil.find_label(main.level_screen, "◈ ")
|
||||
if gp_label == null or not gp_label.is_visible_in_tree():
|
||||
_problems.append("GP 徽章不可见(关卡画面顶栏)")
|
||||
elif main.level_screen.get("_runner") != null:
|
||||
@@ -75,8 +69,8 @@ func _ready() -> void:
|
||||
await get_tree().process_frame
|
||||
await _snap("battle")
|
||||
# 战斗屏文字框/明度带锚点(全局 rect——checker 取样坐标源)
|
||||
_meta["battle_log_hint"] = _find_label_rect(main.stage, "结算明细")
|
||||
_meta["hero_panel"] = _node_rect(_find_by_script(main, "hero_info_panel_view"))
|
||||
_meta["battle_log_hint"] = ProbeUiUtil.label_rect(main.stage, "结算明细")
|
||||
_meta["hero_panel"] = _node_rect(ProbeUiUtil.find_by_script(main, "hero_info_panel_view"))
|
||||
_meta["stage_view"] = _node_rect(main.stage)
|
||||
_meta["board_zone"] = _node_rect(main.board_zone)
|
||||
# draw calls 真机测量(迭代 9 卡⑦——基线 270→目标 <200;窗口化渲染帧读数,
|
||||
@@ -116,59 +110,8 @@ func _snap(key: String) -> void:
|
||||
print("[shot_probe] saved %s (%dx%d)" % [key, img.get_width(), img.get_height()])
|
||||
|
||||
|
||||
func _find_button_by_prefix(root: Node, prefix: String) -> Button:
|
||||
if root == null:
|
||||
return null
|
||||
if root is Button and String(root.text).contains(prefix):
|
||||
return root
|
||||
for c in root.get_children():
|
||||
var r := _find_button_by_prefix(c, prefix)
|
||||
if r != null:
|
||||
return r
|
||||
return null
|
||||
|
||||
|
||||
# 控件实测几何采集(checker 断言坐标源——禁硬编码像素,分辨率档位自适应)
|
||||
func _find_label_rect(root: Node, text_fragment: String) -> Dictionary:
|
||||
if root == null:
|
||||
return {}
|
||||
if root is Label and String(root.text).contains(text_fragment):
|
||||
return {"x": root.global_position.x, "y": root.global_position.y,
|
||||
"w": root.size.x, "h": root.size.y}
|
||||
for c in root.get_children():
|
||||
var r := _find_label_rect(c, text_fragment)
|
||||
if not r.is_empty():
|
||||
return r
|
||||
return {}
|
||||
|
||||
|
||||
func _find_label_by_text(root: Node, text_fragment: String) -> Label:
|
||||
if root == null:
|
||||
return null
|
||||
if root is Label and String(root.text).contains(text_fragment):
|
||||
return root
|
||||
for c in root.get_children():
|
||||
var r := _find_label_by_text(c, text_fragment)
|
||||
if r != null:
|
||||
return r
|
||||
return null
|
||||
|
||||
|
||||
func _node_rect(n: Node) -> Dictionary:
|
||||
if n == null or not (n is CanvasItem):
|
||||
return {}
|
||||
return {"x": n.global_position.x, "y": n.global_position.y,
|
||||
"w": n.size.x, "h": n.size.y}
|
||||
|
||||
|
||||
func _find_by_script(root: Node, script_name: String) -> Node:
|
||||
if root == null:
|
||||
return null
|
||||
var s: Script = root.get_script()
|
||||
if s != null and String(s.resource_path).contains(script_name):
|
||||
return root
|
||||
for c in root.get_children():
|
||||
var r := _find_by_script(c, script_name)
|
||||
if r != null:
|
||||
return r
|
||||
return null
|
||||
@@ -7,6 +7,8 @@
|
||||
# 运行:Godot_console.exe --headless --path . --script res://smoke/skill_rhythm_probe.gd
|
||||
extends SceneTree
|
||||
|
||||
const BattleDriver := preload("res://smoke/battle_driver.gd") # T6 收口:挖掘驱动循环单点(策略仍归 AutoPlayer 单一实现)
|
||||
|
||||
func _initialize() -> void:
|
||||
var db = load("res://core/config_db.gd").new()
|
||||
db.load_tables("res://data/tables", false)
|
||||
@@ -50,23 +52,8 @@ func _run_battle(db, seed_val: int, L: int, depth: int, casts: Dictionary) -> Di
|
||||
casts[sid] = casts.get(sid, 0) + 1)
|
||||
if not settlement.begin_battle({"level_index": L, "node_depth": depth, "hero_hp": int(db.hero.max_hp)}):
|
||||
return out
|
||||
var AutoPlayer = load("res://smoke/auto_player.gd")
|
||||
var guard_rounds := 40
|
||||
while out.result == "" and guard_rounds > 0:
|
||||
guard_rounds -= 1
|
||||
var start_round: int = settlement.round_index
|
||||
var minesweeper = settlement.minesweeper
|
||||
var guard := 400
|
||||
while settlement.action_economy.phase == "Active" and settlement.round_index == start_round and guard > 0 and out.result == "":
|
||||
guard -= 1
|
||||
var plan = AutoPlayer.choose_dig(minesweeper)
|
||||
if plan.is_empty():
|
||||
break
|
||||
if plan.hero_cell != minesweeper.hero_pos():
|
||||
minesweeper.settle_batch([{"action": "move", "cell": plan.hero_cell}])
|
||||
if settlement.action_economy.phase != "Active" or settlement.round_index != start_round:
|
||||
break
|
||||
minesweeper.settle_batch([{"action": "dig", "cell": plan.dig_cell}])
|
||||
if settlement.action_economy.phase == "Active" and settlement.round_index == start_round and out.result == "":
|
||||
minesweeper.settle_batch([{"action": "stop"}])
|
||||
BattleDriver.drive_round(settlement, func() -> bool: return out.result == "")
|
||||
return out
|
||||
+23
-17
@@ -6,15 +6,20 @@
|
||||
extends SceneTree
|
||||
|
||||
const AutoPlayer := preload("res://smoke/auto_player.gd") # O4 收口:自动玩家策略单一实现
|
||||
const ConfigDBScript := preload("res://core/config_db.gd") # T6 收口:表宿主 preload 常量(原 _initialize 运行期 load)
|
||||
|
||||
var _failures := 0
|
||||
var _checks := 0
|
||||
|
||||
|
||||
func check(cond: bool, label: String) -> void:
|
||||
func check(cond: bool, label: String, quiet: bool = false) -> void:
|
||||
_checks += 1
|
||||
if cond:
|
||||
print(" [PASS] " + label)
|
||||
if not quiet:
|
||||
print(" [PASS] " + label)
|
||||
elif quiet:
|
||||
_failures += 1
|
||||
print(" [FAIL] (silent check)") # 原 check_silent 输出逐位保持
|
||||
else:
|
||||
_failures += 1
|
||||
print(" [FAIL] " + label)
|
||||
@@ -22,7 +27,7 @@ func check(cond: bool, label: String) -> void:
|
||||
|
||||
func _initialize() -> void:
|
||||
print("=== Dungeon Sweeper 垂直切片冒烟 ===")
|
||||
var db = load("res://core/config_db.gd").new()
|
||||
var db = ConfigDBScript.new()
|
||||
var load_ok: bool = db.load_tables("res://data/tables", true)
|
||||
check(load_ok, "全部表加载 + 校验通过(含跨表护栏)")
|
||||
if not load_ok:
|
||||
@@ -238,7 +243,7 @@ func _smoke_adventure_api() -> void:
|
||||
# 驱动首战到胜利 → 选路态(非战斗期)→ abandon 受理
|
||||
var autoplay = load("res://smoke/level_autoplay.gd")
|
||||
var battle = adr.runner().current_battle()
|
||||
var st := {"result": "", "rounds": 0, "refreshes": 0, "mine_hits": 0, "hero_dmg": 0, "policy": "normal"}
|
||||
var st := _battle_st("normal")
|
||||
var guard := 30
|
||||
while adr.runner().current_battle() == battle and guard > 0:
|
||||
guard -= 1
|
||||
@@ -950,7 +955,8 @@ func _weaken_refresh_probe() -> Dictionary:
|
||||
settlement._status_effects.apply_effect("weaken", src) # 续时:remaining ← 2,层数不动
|
||||
for eff in settlement._status_effects.query_active("enemy"):
|
||||
if String(eff.effect_id) == "weaken":
|
||||
return {"layers": int(eff.layers), "remaining": int(eff.remaining_settlements), "pool": int(settlement._clamped_pool("enemy_output"))}
|
||||
# T6 收口批:改走伤害控制器公有算术面 clamped_pool(原编排器 _clamped_pool 薄委托的直呼退役)
|
||||
return {"layers": int(eff.layers), "remaining": int(eff.remaining_settlements), "pool": int(settlement._damage.clamped_pool("enemy_output"))}
|
||||
return {}
|
||||
|
||||
|
||||
@@ -1760,7 +1766,7 @@ func _smoke_encounter_roll() -> void:
|
||||
var rng = load("res://core/rng_service.gd").new()
|
||||
rng.set_master_seed(30000 + i)
|
||||
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
|
||||
check_silent(settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.hero.max_hp)}))
|
||||
check(settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.hero.max_hp)}), "遭遇扫描战斗启动(quiet 计数档——计数入总账,成败由后续 spawn 断言归因)", true)
|
||||
counts.append(settlement.presentation_snapshot().enemies.size())
|
||||
# 区间生效:20 个种子应出现 1 和 2(D0 [1,2])
|
||||
check(counts.has(1) and counts.has(2), "D0 区间 [1,2] 两值均出现(分布 %s)" % str(counts))
|
||||
@@ -2417,11 +2423,11 @@ func _has_effect_id(rows: Array, effect_id: String) -> bool:
|
||||
return true
|
||||
return false
|
||||
|
||||
func check_silent(cond: bool) -> void:
|
||||
_checks += 1
|
||||
if not cond:
|
||||
_failures += 1
|
||||
print(" [FAIL] (silent check)")
|
||||
|
||||
# 手搓战斗状态袋单点(T6 收口批——7 处字面量收编;_autoplay_battle 直接消费的最小字段族。
|
||||
# 大字段族同构袋仍走 _adv_st()——_drive_level/_autoplay_battle 全字段消费面)
|
||||
func _battle_st(policy: String) -> Dictionary:
|
||||
return {"result": "", "rounds": 0, "refreshes": 0, "mine_hits": 0, "hero_dmg": 0, "policy": policy}
|
||||
|
||||
|
||||
# ==================== 敌人技能池批(2026-09-16 用户拍板——开轮摇号出招系统) ====================
|
||||
@@ -2883,7 +2889,7 @@ func _smoke_level_graph() -> void:
|
||||
var choice_sizes: Array = []
|
||||
runner.choices_presented.connect(func(c): choice_sizes.append(c.size()))
|
||||
check(runner.start_level(2, 42, {}, 0), "手动驱动:L2 启动(入口战斗直接开打——入口无前置兑付;template_pick=0 固定现行图——精确选路断言)")
|
||||
var st = {"result": "", "rounds": 0, "refreshes": 0, "mine_hits": 0, "hero_dmg": 0, "policy": "normal"}
|
||||
var st = _battle_st("normal")
|
||||
var first = runner.current_battle()
|
||||
var res_box := {} # 信号写回容器(lambda 按值捕获局部变量——Dictionary 引用可变)
|
||||
first.battle_finished.connect(func(p): res_box["result"] = String(p.get("result", "")))
|
||||
@@ -2908,7 +2914,7 @@ func _smoke_level_graph() -> void:
|
||||
for i in range(12):
|
||||
var g_runner = load("res://core/level_runner.gd").new(_db, _fresh_rng(9100 + i))
|
||||
g_runner.start_level(2, 9100 + i)
|
||||
var g_st = {"result": "", "rounds": 0, "refreshes": 0, "mine_hits": 0, "hero_dmg": 0, "policy": "perfect"}
|
||||
var g_st = _battle_st("perfect")
|
||||
var g_first = g_runner.current_battle()
|
||||
var g_box := {}
|
||||
g_first.battle_finished.connect(func(p):
|
||||
@@ -2989,7 +2995,7 @@ func _smoke_level_graph() -> void:
|
||||
var g3_runner = load("res://core/level_runner.gd").new(_db, _fresh_rng(9100 + i))
|
||||
g3_runner.set_hero_override(200, 100)
|
||||
g3_runner.start_level(3, 9100 + i)
|
||||
var g3_st = {"result": "", "rounds": 0, "refreshes": 0, "mine_hits": 0, "hero_dmg": 0, "policy": "perfect"}
|
||||
var g3_st = _battle_st("perfect")
|
||||
var g3_first = g3_runner.current_battle()
|
||||
var g3_box := {}
|
||||
g3_first.battle_finished.connect(func(p):
|
||||
@@ -3035,7 +3041,7 @@ func _smoke_gp_wallet() -> void:
|
||||
var rows: Array = []
|
||||
battle.step_row.connect(func(row): rows.append(row))
|
||||
battle.battle_finished.connect(func(p): fin["p"] = p)
|
||||
var st := {"result": "", "rounds": 0, "refreshes": 0, "mine_hits": 0, "hero_dmg": 0, "policy": "normal"}
|
||||
var st := _battle_st("normal")
|
||||
var guard := 30
|
||||
while runner.current_battle() == battle and not fin.has("p") and guard > 0:
|
||||
guard -= 1
|
||||
@@ -3199,7 +3205,7 @@ func _smoke_shop_room() -> void:
|
||||
var runner = load("res://core/level_runner.gd").new(_db, _fresh_rng(s))
|
||||
runner.set_hero_override(900, 400) # 行进段快速通关档(购买断言与百分比轴正交——加算区单独断言)
|
||||
runner.start_level(2, s, {}, 0) # 固定模板 0——商店经济断言按现行图 GP 账本
|
||||
var st := {"result": "", "rounds": 0, "refreshes": 0, "mine_hits": 0, "hero_dmg": 0, "policy": "normal"}
|
||||
var st := _battle_st("normal")
|
||||
var guard := 40
|
||||
while String(runner.level_result) == "" and int(runner.current_node_id()) != 8 and guard > 0:
|
||||
guard -= 1
|
||||
@@ -3294,7 +3300,7 @@ func _smoke_shop_room() -> void:
|
||||
var b_end = runner.current_battle()
|
||||
if b_end == null:
|
||||
break
|
||||
autoplay._autoplay_battle(runner, b_end, {"result": "", "rounds": 0, "refreshes": 0, "mine_hits": 0, "hero_dmg": 0, "policy": "normal"}, null)
|
||||
autoplay._autoplay_battle(runner, b_end, _battle_st("normal"), null)
|
||||
check(fin_end.has("p") and String(fin_end["p"].get("result", "")) in ["victory", "defeat"],
|
||||
"冒险收束(Boss 战终局:%s)" % String(fin_end.get("p", {}).get("result", "")))
|
||||
check(runner.gp == 0, "冒险收束钱包清零(局内货币生命周期终点——不入存档不入摘要)")
|
||||
|
||||
@@ -11,8 +11,9 @@
|
||||
extends Node
|
||||
|
||||
const AutoPlayer := preload("res://smoke/auto_player.gd") # O4:自动玩家策略单一实现
|
||||
const ProbeUiUtil := preload("res://smoke/probe_ui_util.gd") # T6 收口:树寻址/隔离样板单点
|
||||
|
||||
var main
|
||||
var main: Node
|
||||
var _last_state := {}
|
||||
var _stall_frames := 0
|
||||
var _done := false
|
||||
@@ -41,14 +42,11 @@ func _ready() -> void:
|
||||
var t_ui_ready := Time.get_ticks_msec() - t0
|
||||
# ---- 营地购买合成点击路径(元进度实现批——campbtn/购买/关闭全走真实按钮信号路径;
|
||||
# 隔离存档换注 main.meta 保确定性(禁碰真实档 user://save.json);陈旧档段首清理)----
|
||||
for p0 in ["user://smoke_click_probe_camp.json", "user://smoke_click_probe_camp.json.tmp",
|
||||
"user://smoke_click_probe_reset.json", "user://smoke_click_probe_reset.json.tmp"]:
|
||||
for p0 in ["user://smoke_click_probe_reset.json", "user://smoke_click_probe_reset.json.tmp"]:
|
||||
if FileAccess.file_exists(p0):
|
||||
DirAccess.remove_absolute(p0)
|
||||
var iso_c = load("res://core/save_manager.gd").new("user://smoke_click_probe_camp.json")
|
||||
iso_c.load_from_disk()
|
||||
main.meta = load("res://core/meta_progression.gd").new(main.db, iso_c)
|
||||
var campbtn: Button = _find_button_by_prefix(main._start_overlay, "营地")
|
||||
DirAccess.remove_absolute(p0) # 重置档段前清场兜底(营地/设置段不触该档——防陈旧断点泄入后续段)
|
||||
var iso_c = ProbeUiUtil.isolate_save(main, "smoke_click_probe_camp", false, false, false, false)
|
||||
var campbtn: Button = ProbeUiUtil.find_button(main._start_overlay, "营地", "prefix")
|
||||
if campbtn == null:
|
||||
_problems.append("营地入口按钮缺失(开始层 campbtn)")
|
||||
else:
|
||||
@@ -90,7 +88,7 @@ func _ready() -> void:
|
||||
iso_s.load_settings()
|
||||
main.settings_store = iso_s
|
||||
main.settings_overlay.retarget(iso_s)
|
||||
var settings_btn: Button = _find_button_by_prefix(main._start_overlay, "设置")
|
||||
var settings_btn: Button = ProbeUiUtil.find_button(main._start_overlay, "设置", "prefix")
|
||||
if settings_btn == null:
|
||||
_problems.append("设置入口按钮缺失(开始层 settingsbtn)")
|
||||
else:
|
||||
@@ -100,7 +98,7 @@ func _ready() -> void:
|
||||
_problems.append("设置面未打开(点击路径)")
|
||||
else:
|
||||
# 减动效转正双态(卡④ AC③):开=0.5 引擎态即时生效+落盘持久;关=1.0 回常态
|
||||
var motion_btn: Button = _find_button_by_text(main.settings_overlay, "减动效") # 二轮 D2——开关已换 UiKit toggle 按钮形态
|
||||
var motion_btn: Button = ProbeUiUtil.find_button(main.settings_overlay, "减动效", "contains") # 二轮 D2——开关已换 UiKit toggle 按钮形态
|
||||
if motion_btn == null:
|
||||
_problems.append("设置面减动效开关缺失")
|
||||
else:
|
||||
@@ -127,10 +125,10 @@ func _ready() -> void:
|
||||
if absf(iso_s.volume_linear - 0.3) > 0.001:
|
||||
_problems.append("音量未入 store(%f ≠ 0.3)" % iso_s.volume_linear)
|
||||
# 调试入口默认隐藏门控(卡④ AC④——探针环境 DS_DEBUG 未设)
|
||||
var dbg_btn: Button = _find_button_by_prefix(main._start_overlay, "调试")
|
||||
var dbg_btn: Button = ProbeUiUtil.find_button(main._start_overlay, "调试", "prefix")
|
||||
if dbg_btn != null and dbg_btn.visible:
|
||||
_problems.append("调试入口未隐藏(DS_DEBUG 门控失守——卡④ AC④)")
|
||||
var settings_close: Button = _find_button_by_prefix(main.settings_overlay, "返回")
|
||||
var settings_close: Button = ProbeUiUtil.find_button(main.settings_overlay, "返回", "prefix")
|
||||
if settings_close == null:
|
||||
_problems.append("设置面返回按钮缺失")
|
||||
else:
|
||||
@@ -139,25 +137,17 @@ func _ready() -> void:
|
||||
if main.settings_overlay.visible or not main._start_overlay.visible:
|
||||
_problems.append("设置面关闭后未恢复开始层")
|
||||
# 旧入口退役断言(卡④ AC③——测试调整板不再含减动效开关)
|
||||
var legacy_motion = _find_checkbutton_by_text(main.tune_panel, "减动效")
|
||||
var legacy_motion = ProbeUiUtil.find_check_button(main.tune_panel, "减动效")
|
||||
if legacy_motion != null:
|
||||
_problems.append("调整板减动效旧入口未退役(设置面唯一入口失守——卡④ AC③)")
|
||||
# D1 断言(二轮):退出游戏只在一级菜单——开始菜单有、设置面无
|
||||
if _find_button_by_prefix(main._start_overlay, "退出游戏") == null:
|
||||
if ProbeUiUtil.find_button(main._start_overlay, "退出游戏", "prefix") == null:
|
||||
_problems.append("开始菜单缺退出游戏按钮(二轮 D1)")
|
||||
if _find_button_by_prefix(main.settings_overlay, "退出游戏") != null:
|
||||
if ProbeUiUtil.find_button(main.settings_overlay, "退出游戏", "prefix") != null:
|
||||
_problems.append("设置面仍含退出游戏按钮(二轮 D1——只在一级菜单出现)")
|
||||
# 换回全新零档(营地购买态不泄入后续冒险段——确定性;档案残留下次段首清理兜底)
|
||||
for p0 in ["user://smoke_click_probe_reset.json", "user://smoke_click_probe_reset.json.tmp"]:
|
||||
if FileAccess.file_exists(p0):
|
||||
DirAccess.remove_absolute(p0) # 段首清场——上次运行的残留断点会触发互斥确认早退
|
||||
var iso_reset = load("res://core/save_manager.gd").new("user://smoke_click_probe_reset.json")
|
||||
iso_reset.load_from_disk()
|
||||
iso_reset.mark_first_adventure_done() # 对齐基线(真实档已有游玩史)——非首次脚本模式=逐关抽签模板
|
||||
main.save_manager = iso_reset # 迭代 8 卡①——冒险断点写盘同步隔离(禁碰真实档 user://save.json)
|
||||
main.meta = load("res://core/meta_progression.gd").new(main.db, iso_reset)
|
||||
main._refresh_resume_button()
|
||||
var resume_btn0: Button = _find_button_by_prefix(main._start_overlay, "▶ 继续上局")
|
||||
ProbeUiUtil.isolate_save(main, "smoke_click_probe_reset", true, false, true) # 零档换注 + 继续上局按钮重刷(原三行样板收口)
|
||||
var resume_btn0: Button = ProbeUiUtil.find_button(main._start_overlay, "▶ 继续上局", "prefix")
|
||||
if resume_btn0 == null:
|
||||
_problems.append("继续上局按钮缺失(开始层 resumebtn——装配器)")
|
||||
elif resume_btn0.visible:
|
||||
@@ -227,7 +217,7 @@ func _ready() -> void:
|
||||
_problems.append("大奖励屏打开时输入未锁定")
|
||||
_inter_confirms += 1
|
||||
if _inter_confirms == 1:
|
||||
var next_btn: Button = _find_button_by_prefix(main._inter_overlay, "进入下一层")
|
||||
var next_btn: Button = ProbeUiUtil.find_button(main._inter_overlay, "进入下一层", "prefix")
|
||||
if next_btn == null:
|
||||
_problems.append("大奖励屏确认按钮缺失")
|
||||
else:
|
||||
@@ -309,11 +299,11 @@ func _ready() -> void:
|
||||
await get_tree().process_frame
|
||||
if not main._start_overlay.visible:
|
||||
_problems.append("断点段:终局返回未回开始菜单")
|
||||
var resume_btn: Button = _find_button_by_prefix(main._start_overlay, "▶ 继续上局")
|
||||
var resume_btn: Button = ProbeUiUtil.find_button(main._start_overlay, "▶ 继续上局", "prefix")
|
||||
if resume_btn != null and resume_btn.visible:
|
||||
_problems.append("断点段:退出清断点后继续上局按钮未隐藏(卡②/T3 AC③)")
|
||||
# ② 中断→续玩:新冒险达入口停点=断点在档 → 合成回菜单(模拟关游戏重开)→ 按钮点亮 → 点击恢复至停点
|
||||
var lv_btn: Button = _find_button_by_prefix(main._start_overlay, "▶ 开始冒险")
|
||||
var lv_btn: Button = ProbeUiUtil.find_button(main._start_overlay, "▶ 开始冒险", "prefix")
|
||||
if lv_btn == null or resume_btn == null:
|
||||
_problems.append("断点段:开始冒险/继续上局按钮缺失")
|
||||
else:
|
||||
@@ -356,7 +346,7 @@ func _ready() -> void:
|
||||
await get_tree().process_frame
|
||||
if not main.confirm_dialog.is_open() or main.adventure != null:
|
||||
_problems.append("断点段:有断点点开始冒险未弹确认框(二轮 A 失守)")
|
||||
var cancel_btn: Button = _find_button_by_text(main.confirm_dialog, "取消")
|
||||
var cancel_btn: Button = ProbeUiUtil.find_button(main.confirm_dialog, "取消", "contains")
|
||||
if cancel_btn == null:
|
||||
_problems.append("断点段:确认框取消按钮缺失")
|
||||
else:
|
||||
@@ -366,7 +356,7 @@ func _ready() -> void:
|
||||
_problems.append("断点段:取消未回开始菜单(二轮 A)")
|
||||
lv_btn.pressed.emit()
|
||||
await get_tree().process_frame
|
||||
var confirm_btn: Button = _find_button_by_text(main.confirm_dialog, "开新冒险")
|
||||
var confirm_btn: Button = ProbeUiUtil.find_button(main.confirm_dialog, "开新冒险", "contains")
|
||||
if confirm_btn == null:
|
||||
_problems.append("断点段:确认框开新冒险按钮缺失")
|
||||
else:
|
||||
@@ -561,34 +551,3 @@ func _dump_state() -> void:
|
||||
String(main.runner.current_node().get("type", "")), str(main.bonus_overlay.visible),
|
||||
str(main._inter_overlay.visible), str(main.shop_overlay.visible),
|
||||
str(main.runner.next_candidate_ids())])
|
||||
|
||||
|
||||
# 开始层按钮按文案前缀寻址(campbtn 句柄未存组合根成员——树遍历只读;与 ui_probe 同款助手)
|
||||
func _find_button_by_prefix(root: Node, prefix: String) -> Button:
|
||||
if root is Button and String(root.text).begins_with(prefix):
|
||||
return root
|
||||
for child in root.get_children():
|
||||
var found := _find_button_by_prefix(child, prefix)
|
||||
if found != null:
|
||||
return found
|
||||
return null
|
||||
|
||||
|
||||
func _find_button_by_text(root: Node, needle: String) -> Button:
|
||||
if root is Button and String(root.text).contains(needle):
|
||||
return root
|
||||
for child in root.get_children():
|
||||
var found := _find_button_by_text(child, needle)
|
||||
if found != null:
|
||||
return found
|
||||
return null
|
||||
|
||||
|
||||
func _find_checkbutton_by_text(root: Node, needle: String) -> CheckButton:
|
||||
if root is CheckButton and String(root.text).contains(needle):
|
||||
return root
|
||||
for child in root.get_children():
|
||||
var found := _find_checkbutton_by_text(child, needle)
|
||||
if found != null:
|
||||
return found
|
||||
return null
|
||||
@@ -13,7 +13,9 @@
|
||||
# 运行:Godot_console.exe --headless --path . res://smoke/ui_input_probe.tscn
|
||||
extends Node
|
||||
|
||||
var main
|
||||
const ProbeUiUtil := preload("res://smoke/probe_ui_util.gd") # T6 收口:隔离样板单点
|
||||
|
||||
var main: Node
|
||||
var _problems: Array = []
|
||||
var _actions: Array = [] # action_requested 收集([cell, kind])
|
||||
var _chords: Array = [] # chord_attempted 收集
|
||||
@@ -26,16 +28,9 @@ func _ready() -> void:
|
||||
add_child(main)
|
||||
await get_tree().process_frame
|
||||
# 存档隔离(迭代 8 卡①②——断点写盘面随冒险挂 main.save_manager;探针禁碰真实档 user://save.json,
|
||||
# 同 ui_click_probe 隔离纪律。首次冒险布尔位置位=对齐真实档游玩史口径,模板抽签路径不变)
|
||||
for p0 in ["user://smoke_input_probe_save.json", "user://smoke_input_probe_save.json.tmp"]:
|
||||
if FileAccess.file_exists(p0):
|
||||
DirAccess.remove_absolute(p0) # 段首清场——上次运行的残留断点会触发互斥确认早退
|
||||
var iso = load("res://core/save_manager.gd").new("user://smoke_input_probe_save.json")
|
||||
iso.load_from_disk()
|
||||
iso.mark_first_adventure_done()
|
||||
main.save_manager = iso
|
||||
main.meta = load("res://core/meta_progression.gd").new(main.db, iso)
|
||||
main._refresh_resume_button()
|
||||
# 同 ui_click_probe 隔离纪律。首次冒险布尔位置位=对齐真实档游玩史口径,模板抽签路径不变;
|
||||
# isolate_save 先删后建——上次运行的残留断点会触发互斥确认早退)
|
||||
ProbeUiUtil.isolate_save(main, "smoke_input_probe_save", true, false, true)
|
||||
main._start_adventure_from_menu()
|
||||
await get_tree().process_frame
|
||||
# ---- ⑫ 图上命中真实输入(关卡画面实装批 2026-09-28 回归闸):入口停点候选节点中心
|
||||
@@ -99,6 +94,8 @@ func _ready() -> void:
|
||||
var maxl2 := 0
|
||||
for k2 in layer2.keys():
|
||||
maxl2 = maxi(maxl2, int(layer2[k2]))
|
||||
# 常数锚(画布拟合探针):70/122/26/8 与 presentation/views/level_map_screen.gd 的 _content_size
|
||||
# 公式(70 + maxl*122 + NODE_HALF.x + 8)同源复刻——改常数须两侧同步(本探针只读断言层,真源在屏)
|
||||
var need_x2: float = 70.0 + maxl2 * 122.0 + 26.0 + 8.0
|
||||
if float(ro2._canvas.custom_minimum_size.x) < need_x2 - 0.5:
|
||||
_pre_fail("画布拟合不足(min.x=%.0f < 最深内容 %.0f——深层节点不可点回归)" % [ro2._canvas.custom_minimum_size.x, need_x2])
|
||||
|
||||
@@ -4,22 +4,17 @@
|
||||
# 运行:Godot_console.exe --headless --path . res://smoke/ui_layout_probe.tscn
|
||||
extends Node
|
||||
|
||||
const ProbeUiUtil := preload("res://smoke/probe_ui_util.gd") # T6 收口:隔离样板单点
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
await get_tree().process_frame # autoload 就绪
|
||||
var main = load("res://presentation/main.tscn").instantiate()
|
||||
var main: Node = load("res://presentation/main.tscn").instantiate()
|
||||
add_child(main)
|
||||
await get_tree().process_frame
|
||||
# 存档隔离(迭代 8 卡①②——停点断点写盘随 main.save_manager;探针禁碰真实档,同族探针隔离纪律)
|
||||
for p0 in ["user://smoke_layout_probe_save.json", "user://smoke_layout_probe_save.json.tmp"]:
|
||||
if FileAccess.file_exists(p0):
|
||||
DirAccess.remove_absolute(p0) # 段首清场——上次运行的残留断点会触发互斥确认早退
|
||||
var iso = load("res://core/save_manager.gd").new("user://smoke_layout_probe_save.json")
|
||||
iso.load_from_disk()
|
||||
iso.mark_first_adventure_done()
|
||||
main.save_manager = iso
|
||||
main.meta = load("res://core/meta_progression.gd").new(main.db, iso)
|
||||
main._refresh_resume_button()
|
||||
# 存档隔离(迭代 8 卡①②——停点断点写盘随 main.save_manager;探针禁碰真实档,同族探针隔离纪律;
|
||||
# isolate_save 先删后建——上次运行的残留断点会触发互斥确认早退)
|
||||
ProbeUiUtil.isolate_save(main, "smoke_layout_probe_save", true, false, true)
|
||||
main._start_adventure_from_menu()
|
||||
for i in range(3):
|
||||
await get_tree().process_frame
|
||||
|
||||
+8
-26
@@ -7,10 +7,12 @@
|
||||
# 运行:Godot_console.exe --headless --path . res://smoke/ui_probe.tscn
|
||||
extends Node
|
||||
|
||||
const ProbeUiUtil := preload("res://smoke/probe_ui_util.gd") # T6 收口:树寻址/隔离样板单点
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
await get_tree().process_frame # autoload 就绪
|
||||
var main = load("res://presentation/main.tscn").instantiate()
|
||||
var main: Node = load("res://presentation/main.tscn").instantiate()
|
||||
add_child(main)
|
||||
await get_tree().process_frame
|
||||
var problems: Array = []
|
||||
@@ -20,11 +22,10 @@ func _ready() -> void:
|
||||
get_tree().quit(1)
|
||||
return
|
||||
# ---- 营地覆盖层(元进度实现批——主菜单入口/开合/余额文案/购买禁用态;
|
||||
# 隔离存档换注 main.meta 保确定性(禁碰真实档 user://save.json——SaveManager 路径注入纪律)----
|
||||
var iso = load("res://core/save_manager.gd").new("user://smoke_ui_probe_camp.json")
|
||||
iso.load_from_disk()
|
||||
main.meta = load("res://core/meta_progression.gd").new(main.db, iso)
|
||||
var campbtn: Button = _find_button_by_prefix(main._start_overlay, "营地")
|
||||
# 隔离存档换注 main.meta 保确定性(禁碰真实档 user://save.json——SaveManager 路径注入纪律;
|
||||
# 营地先行段只换 meta 不换 save_manager——冒险段换注零档在前)----
|
||||
ProbeUiUtil.isolate_save(main, "smoke_ui_probe_camp", false, false, false, false)
|
||||
var campbtn: Button = ProbeUiUtil.find_button(main._start_overlay, "营地", "prefix")
|
||||
if campbtn == null:
|
||||
problems.append("营地入口按钮缺失(开始层 campbtn)")
|
||||
else:
|
||||
@@ -52,15 +53,7 @@ func _ready() -> void:
|
||||
if main.camp_overlay.visible or not main._start_overlay.visible:
|
||||
problems.append("营地关闭后未恢复开始层(开合往返失守)")
|
||||
# 换回全新零档(营地段购买态不泄入后续冒险段——确定性;先删后建防陈旧档泄入)
|
||||
for p in ["user://smoke_ui_probe_camp.json", "user://smoke_ui_probe_camp.json.tmp"]:
|
||||
if FileAccess.file_exists(p):
|
||||
DirAccess.remove_absolute(p)
|
||||
var iso_zero = load("res://core/save_manager.gd").new("user://smoke_ui_probe_camp.json")
|
||||
iso_zero.load_from_disk()
|
||||
iso_zero.mark_first_adventure_done() # 对齐真实档游玩史口径(模板抽签路径不变)
|
||||
main.save_manager = iso_zero # 迭代 8 卡①——断点写盘面同步隔离(禁碰真实档)
|
||||
main.meta = load("res://core/meta_progression.gd").new(main.db, iso_zero)
|
||||
main._refresh_resume_button()
|
||||
ProbeUiUtil.isolate_save(main, "smoke_ui_probe_camp", true, false, true)
|
||||
main._start_adventure_from_menu()
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame
|
||||
@@ -244,14 +237,3 @@ func _ready() -> void:
|
||||
push_error("[ui_probe] FAIL — " + p)
|
||||
print("[ui_probe] FAIL — " + "; ".join(problems))
|
||||
get_tree().quit(1)
|
||||
|
||||
|
||||
# 开始层按钮按文案前缀寻址(campbtn 句柄未存组合根成员——树遍历只读;text 前缀匹配首枚按钮)
|
||||
func _find_button_by_prefix(root: Node, prefix: String) -> Button:
|
||||
if root is Button and String(root.text).begins_with(prefix):
|
||||
return root
|
||||
for child in root.get_children():
|
||||
var found := _find_button_by_prefix(child, prefix)
|
||||
if found != null:
|
||||
return found
|
||||
return null
|
||||
@@ -6,10 +6,14 @@
|
||||
# 运行:Godot_console.exe --headless --path . res://smoke/ui_tune_probe.tscn
|
||||
extends Node
|
||||
|
||||
const ProbeUiUtil := preload("res://smoke/probe_ui_util.gd") # T6 收口:隔离样板单点
|
||||
|
||||
const SIM_GUARD_FRAMES := 1800 # ⑤ N=10 headless 模拟防挂死帧预算上限(ADR-0004:探针内停滞检测第一道——坏态不等满预算才报,超时即断言失败)
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
await get_tree().process_frame
|
||||
var main = load("res://presentation/main.tscn").instantiate()
|
||||
var main: Node = load("res://presentation/main.tscn").instantiate()
|
||||
add_child(main)
|
||||
await get_tree().process_frame
|
||||
var problems: Array = []
|
||||
@@ -37,7 +41,7 @@ func _ready() -> void:
|
||||
main.tune_panel.set_sliders(100, 0)
|
||||
main.tune_panel.set_sim_count(10)
|
||||
main.tune_panel.press_simulate_for_probe()
|
||||
var guard := 1800 # N=10 headless 模拟防挂死上限(ADR-0004:探针内停滞检测第一道)
|
||||
var guard := SIM_GUARD_FRAMES
|
||||
while main.tune_panel.is_running() and guard > 0:
|
||||
guard -= 1
|
||||
await get_tree().process_frame
|
||||
@@ -59,15 +63,9 @@ func _ready() -> void:
|
||||
problems.append("hero 表被运行时改写(E4 违例:atk=%s hp=%s,期望恒基线 %d/%d)" % [str(main.db.hero.atk), str(main.db.hero.max_hp), base_atk, base_hp])
|
||||
# ⑧ 单次加成生效回归断言:应用加成 + 启动单场战斗 → 呈现快照 hero 必须是"基线 × 单次加成"
|
||||
# (攻击 ×2 / 生命 +50%)——旧缺陷(残留值被当基线再乘)在 encounter 参数链上同样被抓获
|
||||
# 存档隔离(迭代 8 卡①——停点断点写盘随 main.save_manager;探针禁碰真实档,同族探针隔离纪律)
|
||||
for p0 in ["user://smoke_tune_probe_save.json", "user://smoke_tune_probe_save.json.tmp"]:
|
||||
if FileAccess.file_exists(p0):
|
||||
DirAccess.remove_absolute(p0) # 段首清场——上次运行的残留断点会触发互斥确认早退
|
||||
var iso_tune = load("res://core/save_manager.gd").new("user://smoke_tune_probe_save.json")
|
||||
iso_tune.load_from_disk()
|
||||
iso_tune.mark_first_adventure_done()
|
||||
main.save_manager = iso_tune
|
||||
main.meta = load("res://core/meta_progression.gd").new(main.db, iso_tune)
|
||||
# 存档隔离(迭代 8 卡①——停点断点写盘随 main.save_manager;探针禁碰真实档,同族探针隔离纪律;
|
||||
# isolate_save 先删后建——上次运行的残留断点会触发互斥确认早退)
|
||||
ProbeUiUtil.isolate_save(main, "smoke_tune_probe_save", true, false, false)
|
||||
main.tune_panel.set_sliders(100, 50)
|
||||
main.tune_panel.press_apply_for_probe()
|
||||
main._start_adventure_from_menu() # 完整冒险路径(唯一试玩入口;_start_single 按语义清零滑条 = 恒基线,不承载加成断言)
|
||||
|
||||
@@ -16,15 +16,15 @@ const DamageControllerScript := preload("res://core/damage_controller.gd")
|
||||
|
||||
const TEST_SEED := 20260928 # 夹具种子(基建值非玩法数值——回放确定性契约的输入)
|
||||
|
||||
var _failures := 0
|
||||
var _checks := 0
|
||||
const Kit := preload("res://tests/unit/test_kit.gd") # T6 收口:_check/汇总退出样板单点
|
||||
|
||||
var _rows: Array = [] # 当前 run 的行协议收集(_on_row 绑定法——避 lambda 多行控制流坑)
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
var db = ConfigDBScript.new()
|
||||
var load_ok: bool = db.load_tables("res://data/tables", true)
|
||||
_check("前置:表加载零 error", load_ok and db.diagnostics_count("error") == 0)
|
||||
Kit.check("前置:表加载零 error", load_ok and db.diagnostics_count("error") == 0)
|
||||
if load_ok:
|
||||
_test_five_recipes_and_replay(db)
|
||||
_test_policy_immediate_mode(db)
|
||||
@@ -33,21 +33,7 @@ func _init() -> void:
|
||||
_test_hero_death_halt(db)
|
||||
_test_ac_merge(db)
|
||||
_test_pool_clamp(db)
|
||||
if _failures > 0:
|
||||
push_error("[damage_controller_test] %d/%d 项失败" % [_failures, _checks])
|
||||
quit(1)
|
||||
return
|
||||
print("[damage_controller_test] all green (%d checks)" % _checks)
|
||||
quit(0)
|
||||
|
||||
|
||||
func _check(desc: String, ok: bool) -> void:
|
||||
_checks += 1
|
||||
if ok:
|
||||
print(" [PASS] " + desc)
|
||||
else:
|
||||
_failures += 1
|
||||
push_error(" [FAIL] " + desc)
|
||||
quit(Kit.finish("damage_controller_test"))
|
||||
|
||||
|
||||
# ---- 夹具 ----
|
||||
@@ -84,7 +70,7 @@ func _run_recipe_sequence(db) -> Array:
|
||||
var units = h.units
|
||||
var ctrl = h.ctrl
|
||||
if not h.spawn_ok:
|
||||
_check("前置:units.spawn(1,0) 成功(遭遇行在案)", false)
|
||||
Kit.check("前置:units.spawn(1,0) 成功(遭遇行在案)", false)
|
||||
return []
|
||||
var hero_before: int = int(units.hero_unit().hp)
|
||||
var enemy_hp_before: Array = []
|
||||
@@ -122,27 +108,27 @@ func _run_recipe_sequence(db) -> Array:
|
||||
for id in [t1, t2, t3, t4, t5]:
|
||||
if ctrl.outcome(int(id)) != "redeemed":
|
||||
outcomes_redeemed = false
|
||||
_check("五配方单据 flush 后全部 redeemed(B 模式段界结账)", outcomes_redeemed)
|
||||
Kit.check("五配方单据 flush 后全部 redeemed(B 模式段界结账)", outcomes_redeemed)
|
||||
|
||||
var hero_after: int = int(units.hero_unit().hp)
|
||||
var r2: Dictionary = _row_by_display_prefix("T2敌方技")
|
||||
var r5: Dictionary = _row_by_display_prefix("T5反噬")
|
||||
_check("主角扣血 = 敌方技行值 + 反噬行值(逐行归因闭合)",
|
||||
Kit.check("主角扣血 = 敌方技行值 + 反噬行值(逐行归因闭合)",
|
||||
hero_before - hero_after == int(r2.get("value", -1)) + int(r5.get("value", -1)))
|
||||
_check("敌方技行值 ≥ 下限 + rider(rider 后置不进乘区)", int(r2.get("value", 0)) >= floor_v + floor_v)
|
||||
_check("反噬行值 = 落地下限(中性无类型表值)", int(r5.get("value", 0)) == floor_v)
|
||||
Kit.check("敌方技行值 ≥ 下限 + rider(rider 后置不进乘区)", int(r2.get("value", 0)) >= floor_v + floor_v)
|
||||
Kit.check("反噬行值 = 落地下限(中性无类型表值)", int(r5.get("value", 0)) == floor_v)
|
||||
|
||||
var r1: Dictionary = _row_by_display_prefix("T1主角攻击")
|
||||
var ex1: Dictionary = r1.get("extra", {})
|
||||
_check("主角攻击行 AC = 防守方 AC + 攻击加成(开单前快照一致)", int(ex1.get("ac", -1)) == threshold_expected)
|
||||
_check("掷点 ∈ [1, 骰面]", int(ex1.get("roll", -1)) >= 1 and int(ex1.get("roll", -1)) <= int(h.db.settlement.roll_faces))
|
||||
Kit.check("主角攻击行 AC = 防守方 AC + 攻击加成(开单前快照一致)", int(ex1.get("ac", -1)) == threshold_expected)
|
||||
Kit.check("掷点 ∈ [1, 骰面]", int(ex1.get("roll", -1)) >= 1 and int(ex1.get("roll", -1)) <= int(h.db.settlement.roll_faces))
|
||||
var tier_ok := false
|
||||
for dv in h.db.settlement.roll_tier_discounts:
|
||||
if int(dv) == int(ex1.get("tier", -1)):
|
||||
tier_ok = true
|
||||
if int(ex1.get("tier", -1)) == 100:
|
||||
tier_ok = true
|
||||
_check("掷点档位 ∈ 表档位值 ∪ 满额", tier_ok)
|
||||
Kit.check("掷点档位 ∈ 表档位值 ∪ 满额", tier_ok)
|
||||
|
||||
var r4: Dictionary = _row_by_display_prefix("T4群伤")
|
||||
var alive_at_blast: int = int(r4.get("extra", {}).get("count", -1))
|
||||
@@ -158,10 +144,10 @@ func _run_recipe_sequence(db) -> Array:
|
||||
var attack_landed: int = int(r1.get("value", 0))
|
||||
var typed_landed: int = int(_row_by_display_prefix("T3克制直伤").get("value", 0))
|
||||
if any_enemy_dead:
|
||||
_check("敌方总扣血 ≥ 攻击+克制+群伤(死亡溢出合法,下界归因)",
|
||||
Kit.check("敌方总扣血 ≥ 攻击+克制+群伤(死亡溢出合法,下界归因)",
|
||||
enemy_drop_total >= attack_landed + typed_landed + int(r4.get("value", 0)) * maxi(0, alive_at_blast))
|
||||
else:
|
||||
_check("敌方总扣血 = 攻击 + 克制直伤 + 群伤×存活数(逐行归因闭合)",
|
||||
Kit.check("敌方总扣血 = 攻击 + 克制直伤 + 群伤×存活数(逐行归因闭合)",
|
||||
enemy_drop_total == attack_landed + typed_landed + int(r4.get("value", 0)) * alive_at_blast)
|
||||
|
||||
var protocol_ok := true
|
||||
@@ -174,7 +160,7 @@ func _run_recipe_sequence(db) -> Array:
|
||||
protocol_ok = false
|
||||
if not (r.extra is Dictionary) or not r.extra.has("target"):
|
||||
protocol_ok = false
|
||||
_check("行协议公共字段封闭(type/source_class/display/value≥下限/extra.target)", protocol_ok)
|
||||
Kit.check("行协议公共字段封闭(type/source_class/display/value≥下限/extra.target)", protocol_ok)
|
||||
|
||||
# 复现序列(供双跑对拍):逐行 (display, value)
|
||||
var seq: Array = []
|
||||
@@ -185,9 +171,9 @@ func _run_recipe_sequence(db) -> Array:
|
||||
|
||||
func _test_five_recipes_and_replay(db) -> void:
|
||||
var run1: Array = _run_recipe_sequence(db)
|
||||
_check("五配方序列产出 5 行伤害行", run1.size() == 5)
|
||||
Kit.check("五配方序列产出 5 行伤害行", run1.size() == 5)
|
||||
var run2: Array = _run_recipe_sequence(db)
|
||||
_check("同种子双跑行序列逐位一致(回放确定性契约)", run1 == run2)
|
||||
Kit.check("同种子双跑行序列逐位一致(回放确定性契约)", run1 == run2)
|
||||
|
||||
|
||||
# ---- A 模式(开单即结——回切位) ----
|
||||
@@ -201,8 +187,8 @@ func _test_policy_immediate_mode(db) -> void:
|
||||
var floor_v: int = int(h.db.settlement.damage_floor)
|
||||
var t: int = h.ctrl.submit(h.ctrl.open_ticket(DamageControllerScript.KIND_BOARD_FLAT_HERO, "mine_hit",
|
||||
"A模式反噬", "棋盘兑付", floor_v, DamageControllerScript.TARGET_HERO, 1))
|
||||
_check("A 模式开单即结(无 flush 即 redeemed)", h.ctrl.outcome(t) == "redeemed")
|
||||
_check("A 模式主角即时扣血", int(h.units.hero_unit().hp) == int(h.db.hero.max_hp) - floor_v)
|
||||
Kit.check("A 模式开单即结(无 flush 即 redeemed)", h.ctrl.outcome(t) == "redeemed")
|
||||
Kit.check("A 模式主角即时扣血", int(h.units.hero_unit().hp) == int(h.db.hero.max_hp) - floor_v)
|
||||
|
||||
|
||||
# ---- 来源敌死亡:队列中未结单据作废(死亡即不动手) ----
|
||||
@@ -221,8 +207,8 @@ func _test_source_death_void(db) -> void:
|
||||
var t: int = h.ctrl.submit(ticket)
|
||||
h.units.apply_damage(src, int(src.max_hp)) # 敌当场倒下 → unit_died → 该敌未结单作废
|
||||
h.ctrl.flush()
|
||||
_check("来源敌死亡后其未结单据 voided(死亡即不动手)", h.ctrl.outcome(t) == "voided")
|
||||
_check("作废单零行零扣血", int(h.units.hero_unit().hp) == hero_before and _rows.is_empty())
|
||||
Kit.check("来源敌死亡后其未结单据 voided(死亡即不动手)", h.ctrl.outcome(t) == "voided")
|
||||
Kit.check("作废单零行零扣血", int(h.units.hero_unit().hp) == hero_before and _rows.is_empty())
|
||||
|
||||
|
||||
# ---- requires 前置门:前置单未实际有效兑付 → 后继单静默作废;无存活敌 blast = 合法零兑付 ----
|
||||
@@ -238,7 +224,7 @@ func _test_requires_gate_and_zero(db) -> void:
|
||||
var big: int = front_hp * 10
|
||||
var t_clear: int = h.ctrl.flush_submit(h.ctrl.open_ticket(DamageControllerScript.KIND_BOARD_BLAST, "clear_reward",
|
||||
"清场群伤", "棋盘兑付", big, DamageControllerScript.TARGET_ALL_ALIVE, 1))
|
||||
_check("flush_submit 同步开单即结(清图批契约)", h.ctrl.outcome(t_clear) == "redeemed" and h.units.query_alive("enemy").is_empty())
|
||||
Kit.check("flush_submit 同步开单即结(清图批契约)", h.ctrl.outcome(t_clear) == "redeemed" and h.units.query_alive("enemy").is_empty())
|
||||
var t_zero: int = h.ctrl.submit(h.ctrl.open_ticket(DamageControllerScript.KIND_BOARD_BLAST, "clear_reward",
|
||||
"空场群伤", "棋盘兑付", floor_v, DamageControllerScript.TARGET_ALL_ALIVE, 1))
|
||||
var gated_ticket: Dictionary = h.ctrl.open_ticket(DamageControllerScript.KIND_HERO_ATTACK, "hero_attack",
|
||||
@@ -246,8 +232,8 @@ func _test_requires_gate_and_zero(db) -> void:
|
||||
gated_ticket["requires"] = t_zero
|
||||
var t_gated: int = h.ctrl.submit(gated_ticket)
|
||||
h.ctrl.flush()
|
||||
_check("无存活敌 blast = 合法零兑付(zero 非 voided)", h.ctrl.outcome(t_zero) == "zero")
|
||||
_check("前置单未实际有效 → 后继单 voided(requires 门)", h.ctrl.outcome(t_gated) == "voided")
|
||||
Kit.check("无存活敌 blast = 合法零兑付(zero 非 voided)", h.ctrl.outcome(t_zero) == "zero")
|
||||
Kit.check("前置单未实际有效 → 后继单 voided(requires 门)", h.ctrl.outcome(t_gated) == "voided")
|
||||
|
||||
|
||||
# ---- 主角倒下:flush 余单全部作废(_halted 终止) ----
|
||||
@@ -263,8 +249,8 @@ func _test_hero_death_halt(db) -> void:
|
||||
var t_after: int = h.ctrl.submit(h.ctrl.open_ticket(DamageControllerScript.KIND_BOARD_FLAT_HERO, "mine_hit",
|
||||
"余单", "棋盘兑付", 1, DamageControllerScript.TARGET_HERO, 2))
|
||||
h.ctrl.flush()
|
||||
_check("致死单 redeemed 且主角 hp 归零", h.ctrl.outcome(t_kill) == "redeemed" and int(h.units.hero_unit().hp) == 0)
|
||||
_check("主角倒下后余单 voided(flush 终止)", h.ctrl.outcome(t_after) == "voided")
|
||||
Kit.check("致死单 redeemed 且主角 hp 归零", h.ctrl.outcome(t_kill) == "redeemed" and int(h.units.hero_unit().hp) == 0)
|
||||
Kit.check("主角倒下后余单 voided(flush 终止)", h.ctrl.outcome(t_after) == "voided")
|
||||
|
||||
|
||||
# ---- AC 合流:override 入参 / 表值兜底 / ac 型状态叠层(不入池) ----
|
||||
@@ -275,10 +261,10 @@ func _test_ac_merge(db) -> void:
|
||||
if not h.spawn_ok:
|
||||
return
|
||||
var base_ac: int = int(h.db.hero.armor_class)
|
||||
_check("缺省兜底:defender_ac(hero) = hero 表值", h.ctrl.defender_ac(h.units.hero_unit()) == base_ac)
|
||||
Kit.check("缺省兜底:defender_ac(hero) = hero 表值", h.ctrl.defender_ac(h.units.hero_unit()) == base_ac)
|
||||
var override_v: int = base_ac + int(h.db.settlement.damage_floor)
|
||||
h.ctrl.set_hero_ac_override(override_v)
|
||||
_check("override 合流入参生效", h.ctrl.defender_ac(h.units.hero_unit()) == override_v)
|
||||
Kit.check("override 合流入参生效", h.ctrl.defender_ac(h.units.hero_unit()) == override_v)
|
||||
# ac 型状态效果叠其上(找 hero 侧 unit=ac 行)
|
||||
var ac_eff := {}
|
||||
for row in h.db.status_effects.rows:
|
||||
@@ -286,12 +272,12 @@ func _test_ac_merge(db) -> void:
|
||||
ac_eff = row
|
||||
break
|
||||
if ac_eff.is_empty():
|
||||
_check("前置:状态表存在 hero 侧 ac 型行", false)
|
||||
Kit.check("前置:状态表存在 hero 侧 ac 型行", false)
|
||||
return
|
||||
var src := {"source_class": "safe_cell", "origin_cell": Vector2i(-1, -1), "round_index": 0}
|
||||
h.effects.apply_effect(String(ac_eff.effect_id), src)
|
||||
var expected: int = override_v + int(ac_eff.magnitude)
|
||||
_check("ac 型效果叠层入 AC(层数×幅度,不入池)", h.ctrl.defender_ac(h.units.hero_unit()) == expected)
|
||||
Kit.check("ac 型效果叠层入 AC(层数×幅度,不入池)", h.ctrl.defender_ac(h.units.hero_unit()) == expected)
|
||||
|
||||
|
||||
# ---- 池乘区收口:池内加算 → clamp;ac 型不入池 ----
|
||||
@@ -303,7 +289,7 @@ func _test_pool_clamp(db) -> void:
|
||||
return
|
||||
var lo: int = int(h.db.settlement.pool_clamp_lo)
|
||||
var hi: int = int(h.db.settlement.pool_clamp_hi)
|
||||
_check("空效果池读 0(表 clamp 域含 0 前提)", h.ctrl.clamped_pool("hero_offense") == 0 and lo <= 0 and hi >= 0)
|
||||
Kit.check("空效果池读 0(表 clamp 域含 0 前提)", h.ctrl.clamped_pool("hero_offense") == 0 and lo <= 0 and hi >= 0)
|
||||
# 找 hero_offense 池的 percent 型 hero 侧效果行
|
||||
var pool_eff := {}
|
||||
for row in h.db.status_effects.rows:
|
||||
@@ -313,14 +299,14 @@ func _test_pool_clamp(db) -> void:
|
||||
pool_eff = row
|
||||
break
|
||||
if pool_eff.is_empty():
|
||||
_check("前置:hero_offense 池存在 percent 型 hero 侧行", false)
|
||||
Kit.check("前置:hero_offense 池存在 percent 型 hero 侧行", false)
|
||||
return
|
||||
var src := {"source_class": "safe_cell", "origin_cell": Vector2i(-1, -1), "round_index": 0}
|
||||
var first_ok: bool = h.effects.apply_effect(String(pool_eff.effect_id), src)
|
||||
var expected1: int = clampi(int(pool_eff.magnitude), lo, hi)
|
||||
_check("池值 = 单实例 L×m 加算后 clamp", first_ok and h.ctrl.clamped_pool("hero_offense") == expected1)
|
||||
Kit.check("池值 = 单实例 L×m 加算后 clamp", first_ok and h.ctrl.clamped_pool("hero_offense") == expected1)
|
||||
var second_ok: bool = h.effects.apply_effect(String(pool_eff.effect_id), src)
|
||||
_check("refresh 政策重复授予受理且层数恒 1(叠层改续时裁决)——池值不变",
|
||||
Kit.check("refresh 政策重复授予受理且层数恒 1(叠层改续时裁决)——池值不变",
|
||||
second_ok and h.ctrl.clamped_pool("hero_offense") == expected1)
|
||||
# ac 型效果不入 percent 池
|
||||
var ac_eff2 := {}
|
||||
@@ -331,7 +317,7 @@ func _test_pool_clamp(db) -> void:
|
||||
if not ac_eff2.is_empty():
|
||||
var before: int = h.ctrl.clamped_pool("hero_offense")
|
||||
h.effects.apply_effect(String(ac_eff2.effect_id), src)
|
||||
_check("ac 型效果不入池(percent 池隔离)", h.ctrl.clamped_pool("hero_offense") == before)
|
||||
Kit.check("ac 型效果不入池(percent 池隔离)", h.ctrl.clamped_pool("hero_offense") == before)
|
||||
# stack 政策效果:全新容器授予至满层 → 再授拒绝(满层拒绝原子——容器零写入)
|
||||
var stack_eff := {}
|
||||
for row3 in h.db.status_effects.rows:
|
||||
@@ -339,7 +325,7 @@ func _test_pool_clamp(db) -> void:
|
||||
stack_eff = row3
|
||||
break
|
||||
if stack_eff.is_empty():
|
||||
_check("前置:状态表存在 stack 政策行", false)
|
||||
Kit.check("前置:状态表存在 stack 政策行", false)
|
||||
return
|
||||
var effects2 = StatusContainerScript.new(h.db)
|
||||
effects2.notify_battle_start()
|
||||
@@ -348,4 +334,4 @@ func _test_pool_clamp(db) -> void:
|
||||
for i in range(max_n + 1):
|
||||
if effects2.apply_effect(String(stack_eff.effect_id), src):
|
||||
granted += 1
|
||||
_check("stack 政策恰授予 max_stack 层后拒绝(满层拒绝原子)", granted == max_n)
|
||||
Kit.check("stack 政策恰授予 max_stack 层后拒绝(满层拒绝原子)", granted == max_n)
|
||||
@@ -6,36 +6,21 @@ extends SceneTree
|
||||
|
||||
const DamageMathScript := preload("res://core/damage_math.gd")
|
||||
const ConfigDBScript := preload("res://core/config_db.gd")
|
||||
const Kit := preload("res://tests/unit/test_kit.gd") # T6 收口:_check/汇总退出样板单点
|
||||
|
||||
var _failures := 0
|
||||
var _checks := 0
|
||||
var db
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
db = ConfigDBScript.new()
|
||||
var load_ok: bool = db.load_tables("res://data/tables", true)
|
||||
_check("前置:settlement 表加载零 error", load_ok and db.diagnostics_count("error") == 0)
|
||||
Kit.check("前置:settlement 表加载零 error", load_ok and db.diagnostics_count("error") == 0)
|
||||
if load_ok:
|
||||
_test_pool_damage()
|
||||
_test_counter_component()
|
||||
_test_roll_discount_percent()
|
||||
_test_rolled_component()
|
||||
if _failures > 0:
|
||||
push_error("[damage_math_test] %d/%d 项失败" % [_failures, _checks])
|
||||
quit(1)
|
||||
return
|
||||
print("[damage_math_test] all green (%d checks)" % _checks)
|
||||
quit(0)
|
||||
|
||||
|
||||
func _check(desc: String, ok: bool) -> void:
|
||||
_checks += 1
|
||||
if ok:
|
||||
print(" [PASS] " + desc)
|
||||
else:
|
||||
_failures += 1
|
||||
push_error(" [FAIL] " + desc)
|
||||
quit(Kit.finish("damage_math_test"))
|
||||
|
||||
|
||||
func _floor_val() -> int:
|
||||
@@ -46,23 +31,23 @@ func _floor_val() -> int:
|
||||
|
||||
func _test_pool_damage() -> void:
|
||||
var d := _floor_val() + 9 # 表值推导的任意正探针量(非玩法行值)
|
||||
_check("中性双池 (0,0) 原样透传", DamageMathScript.pool_damage(d, 0, 0) == d)
|
||||
_check("负敌方池压低不增高", DamageMathScript.pool_damage(d, 0, int(db.settlement.pool_clamp_lo)) <= d)
|
||||
_check("正主角池抬升不低于原值", DamageMathScript.pool_damage(d, int(db.settlement.pool_clamp_hi), 0) >= d)
|
||||
_check("主角池单调不减(+50% vs +49% 探针档)", DamageMathScript.pool_damage(d, 50, 0) >= DamageMathScript.pool_damage(d, 49, 0))
|
||||
_check("对称性:池值互换结果不变(乘算交换)", DamageMathScript.pool_damage(d, 30, -20) == DamageMathScript.pool_damage(d, -20, 30))
|
||||
_check("输出非负(maxi(0,·) 封底)", DamageMathScript.pool_damage(d, int(db.settlement.pool_clamp_lo), int(db.settlement.pool_clamp_lo)) >= 0)
|
||||
Kit.check("中性双池 (0,0) 原样透传", DamageMathScript.pool_damage(d, 0, 0) == d)
|
||||
Kit.check("负敌方池压低不增高", DamageMathScript.pool_damage(d, 0, int(db.settlement.pool_clamp_lo)) <= d)
|
||||
Kit.check("正主角池抬升不低于原值", DamageMathScript.pool_damage(d, int(db.settlement.pool_clamp_hi), 0) >= d)
|
||||
Kit.check("主角池单调不减(+50% vs +49% 探针档)", DamageMathScript.pool_damage(d, 50, 0) >= DamageMathScript.pool_damage(d, 49, 0))
|
||||
Kit.check("对称性:池值互换结果不变(乘算交换)", DamageMathScript.pool_damage(d, 30, -20) == DamageMathScript.pool_damage(d, -20, 30))
|
||||
Kit.check("输出非负(maxi(0,·) 封底)", DamageMathScript.pool_damage(d, int(db.settlement.pool_clamp_lo), int(db.settlement.pool_clamp_lo)) >= 0)
|
||||
|
||||
|
||||
# ---- 克制分量:空桶不吃修正恒 0;落地下限保底([1,+∞) 全来源)----
|
||||
|
||||
func _test_counter_component() -> void:
|
||||
var floor_v := _floor_val()
|
||||
_check("空桶(点数≤0)恒 0 不吃修正", DamageMathScript.counter_component(0, floor_v, floor_v) == 0
|
||||
Kit.check("空桶(点数≤0)恒 0 不吃修正", DamageMathScript.counter_component(0, floor_v, floor_v) == 0
|
||||
and DamageMathScript.counter_component(-1, 5, floor_v) == 0)
|
||||
_check("正向修正直加", DamageMathScript.counter_component(floor_v, floor_v, floor_v) == floor_v * 2)
|
||||
_check("负修正触底保落地伤害下限", DamageMathScript.counter_component(floor_v, -floor_v * 10, floor_v) == floor_v)
|
||||
_check("下限参数缺省 = 0(无下限语义面)", DamageMathScript.counter_component(floor_v, -floor_v * 10) == 0)
|
||||
Kit.check("正向修正直加", DamageMathScript.counter_component(floor_v, floor_v, floor_v) == floor_v * 2)
|
||||
Kit.check("负修正触底保落地伤害下限", DamageMathScript.counter_component(floor_v, -floor_v * 10, floor_v) == floor_v)
|
||||
Kit.check("下限参数缺省 = 0(无下限语义面)", DamageMathScript.counter_component(floor_v, -floor_v * 10) == 0)
|
||||
|
||||
|
||||
# ---- 掷点折扣查档(纯函数):满阈值恒全额;档位边界/末档语义与表数组一致 ----
|
||||
@@ -71,22 +56,22 @@ func _test_roll_discount_percent() -> void:
|
||||
var threshold := 10 # 查档探针阈值(夹具值——档位参数本身全部来自表)
|
||||
var bounds: Array = db.settlement.roll_tier_bounds
|
||||
var discounts: Array = db.settlement.roll_tier_discounts
|
||||
_check("前置:档位数 = 边界数 + 1(查档契约——档数 = 边界数 + 1)", discounts.size() == bounds.size() + 1 and discounts.size() > 0)
|
||||
_check("掷点 ≥ 阈值恒全额 100", DamageMathScript.roll_discount_percent(threshold, threshold, bounds, discounts) == 100
|
||||
Kit.check("前置:档位数 = 边界数 + 1(查档契约——档数 = 边界数 + 1)", discounts.size() == bounds.size() + 1 and discounts.size() > 0)
|
||||
Kit.check("掷点 ≥ 阈值恒全额 100", DamageMathScript.roll_discount_percent(threshold, threshold, bounds, discounts) == 100
|
||||
and DamageMathScript.roll_discount_percent(threshold + 5, threshold, bounds, discounts) == 100)
|
||||
# 缺口 = 1:落首档(缺口 ∈ [1, bounds[0]] → discounts[0])
|
||||
var tier0_expected: int = int(discounts[0])
|
||||
if int(bounds[0]) >= 1:
|
||||
_check("缺口 1 落首档", DamageMathScript.roll_discount_percent(threshold - 1, threshold, bounds, discounts) == tier0_expected)
|
||||
Kit.check("缺口 1 落首档", DamageMathScript.roll_discount_percent(threshold - 1, threshold, bounds, discounts) == tier0_expected)
|
||||
# 缺口超末边界:落末档
|
||||
var last_expected: int = int(discounts[-1])
|
||||
_check("缺口超末边界落末档", DamageMathScript.roll_discount_percent(threshold - int(bounds[-1]) - 1, threshold, bounds, discounts) == last_expected)
|
||||
Kit.check("缺口超末边界落末档", DamageMathScript.roll_discount_percent(threshold - int(bounds[-1]) - 1, threshold, bounds, discounts) == last_expected)
|
||||
# 档位值降序(重档折扣更深)——表自检
|
||||
var descending := true
|
||||
for i in range(discounts.size() - 1):
|
||||
if int(discounts[i]) < int(discounts[i + 1]):
|
||||
descending = false
|
||||
_check("档位值随档位加深不升(折扣单调)", descending)
|
||||
Kit.check("档位值随档位加深不升(折扣单调)", descending)
|
||||
|
||||
|
||||
# ---- 吃掷点分量算术:折扣→(暴击)→floor→+克制修正(单一取整链)----
|
||||
@@ -94,9 +79,9 @@ func _test_roll_discount_percent() -> void:
|
||||
func _test_rolled_component() -> void:
|
||||
var base := float(_floor_val() + 7) # 表值推导探针基线
|
||||
var crit_mult: int = int(db.settlement.crit_multiplier)
|
||||
_check("满额折扣无暴击 = floor(基线) + 修正 0", DamageMathScript.rolled_component(base, 100, false, crit_mult, 0) == int(floor(base)))
|
||||
_check("满额折扣暴击按表倍率放大", DamageMathScript.rolled_component(base, 100, true, crit_mult, 0) == int(floor(base * float(crit_mult) / 100.0)))
|
||||
_check("折扣降低不高于满额", DamageMathScript.rolled_component(base, 50, false, crit_mult, 0) <= DamageMathScript.rolled_component(base, 100, false, crit_mult, 0))
|
||||
_check("克制修正加在 floor 之后(加法核)", DamageMathScript.rolled_component(base, 100, false, crit_mult, _floor_val()) == int(floor(base)) + _floor_val())
|
||||
_check("复合序 = 基线×折扣%×暴击%(floor 只在链末)",
|
||||
Kit.check("满额折扣无暴击 = floor(基线) + 修正 0", DamageMathScript.rolled_component(base, 100, false, crit_mult, 0) == int(floor(base)))
|
||||
Kit.check("满额折扣暴击按表倍率放大", DamageMathScript.rolled_component(base, 100, true, crit_mult, 0) == int(floor(base * float(crit_mult) / 100.0)))
|
||||
Kit.check("折扣降低不高于满额", DamageMathScript.rolled_component(base, 50, false, crit_mult, 0) <= DamageMathScript.rolled_component(base, 100, false, crit_mult, 0))
|
||||
Kit.check("克制修正加在 floor 之后(加法核)", DamageMathScript.rolled_component(base, 100, false, crit_mult, _floor_val()) == int(floor(base)) + _floor_val())
|
||||
Kit.check("复合序 = 基线×折扣%×暴击%(floor 只在链末)",
|
||||
DamageMathScript.rolled_component(base, 50, true, crit_mult, 0) == int(floor(base * 50.0 / 100.0 * float(crit_mult) / 100.0)))
|
||||
@@ -12,35 +12,19 @@ const ConfigDBScript := preload("res://core/config_db.gd")
|
||||
const RngServiceScript := preload("res://core/rng_service.gd")
|
||||
const StatusContainerScript := preload("res://core/status_container.gd")
|
||||
const DamageControllerScript := preload("res://core/damage_controller.gd")
|
||||
|
||||
var _failures := 0
|
||||
var _checks := 0
|
||||
const Kit := preload("res://tests/unit/test_kit.gd") # T6 收口:_check/汇总退出样板单点
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
var db = ConfigDBScript.new()
|
||||
var load_ok: bool = db.load_tables("res://data/tables", true)
|
||||
_check("前置:表加载零 error", load_ok and db.diagnostics_count("error") == 0)
|
||||
Kit.check("前置:表加载零 error", load_ok and db.diagnostics_count("error") == 0)
|
||||
if load_ok:
|
||||
_test_damage_controller_vocabularies()
|
||||
_test_rng_tag_closure(db)
|
||||
_test_ticket_nine_columns()
|
||||
_test_status_boolean_acceptance(db)
|
||||
if _failures > 0:
|
||||
push_error("[system_contracts_test] %d/%d 项失败" % [_failures, _checks])
|
||||
quit(1)
|
||||
return
|
||||
print("[system_contracts_test] all green (%d checks)" % _checks)
|
||||
quit(0)
|
||||
|
||||
|
||||
func _check(desc: String, ok: bool) -> void:
|
||||
_checks += 1
|
||||
if ok:
|
||||
print(" [PASS] " + desc)
|
||||
else:
|
||||
_failures += 1
|
||||
push_error(" [FAIL] " + desc)
|
||||
quit(Kit.finish("system_contracts_test"))
|
||||
|
||||
|
||||
# ---- B1 兑付配方词表:五配方/三目标规则/双结账政策——常量非空且互异(封闭枚举负空间) ----
|
||||
@@ -55,11 +39,11 @@ func _test_damage_controller_vocabularies() -> void:
|
||||
for k in kinds:
|
||||
if String(k).is_empty():
|
||||
nonempty = false
|
||||
_check("五配方常量非空(开单面唯一分派键)", nonempty)
|
||||
Kit.check("五配方常量非空(开单面唯一分派键)", nonempty)
|
||||
var unique_kinds: Dictionary = {}
|
||||
for k in kinds:
|
||||
unique_kinds[String(k)] = true
|
||||
_check("五配方常量互异(词表封闭无别名)", unique_kinds.size() == kinds.size())
|
||||
Kit.check("五配方常量互异(词表封闭无别名)", unique_kinds.size() == kinds.size())
|
||||
var targets: Array = [
|
||||
DamageControllerScript.TARGET_FRONT_ALIVE, DamageControllerScript.TARGET_HERO,
|
||||
DamageControllerScript.TARGET_ALL_ALIVE,
|
||||
@@ -67,8 +51,8 @@ func _test_damage_controller_vocabularies() -> void:
|
||||
var unique_targets: Dictionary = {}
|
||||
for t in targets:
|
||||
unique_targets[String(t)] = true
|
||||
_check("三目标规则常量互异(结账时刻现场解析词表)", unique_targets.size() == targets.size())
|
||||
_check("双结账政策常量互异(A 即结 / B 攒批回切位)",
|
||||
Kit.check("三目标规则常量互异(结账时刻现场解析词表)", unique_targets.size() == targets.size())
|
||||
Kit.check("双结账政策常量互异(A 即结 / B 攒批回切位)",
|
||||
String(DamageControllerScript.POLICY_IMMEDIATE) != String(DamageControllerScript.POLICY_SEGMENT))
|
||||
|
||||
|
||||
@@ -76,9 +60,9 @@ func _test_damage_controller_vocabularies() -> void:
|
||||
|
||||
func _test_rng_tag_closure(db) -> void:
|
||||
var stream_label: String = String(db.settlement.armor_roll_stream)
|
||||
_check("settlement.armor_roll_stream ∈ RngService 封闭标签词表(EC12)",
|
||||
Kit.check("settlement.armor_roll_stream ∈ RngService 封闭标签词表(EC12)",
|
||||
RngServiceScript.PURPOSE_TAGS.has(stream_label))
|
||||
_check("RngService 用途标签词表非空且互异(封闭语义由流改标签)",
|
||||
Kit.check("RngService 用途标签词表非空且互异(封闭语义由流改标签)",
|
||||
RngServiceScript.PURPOSE_TAGS.size() > 0)
|
||||
|
||||
|
||||
@@ -95,9 +79,9 @@ func _test_ticket_nine_columns() -> void:
|
||||
for c in columns:
|
||||
if not t.has(c):
|
||||
all_present = false
|
||||
_check("单据九栏 + 可选两栏缺省齐备(开单面只写差异栏前提)", all_present)
|
||||
_check("缺省无类型轴(attack_type 空 = 恒不查克制矩阵)", String(t.attack_type) == "")
|
||||
_check("缺省无前置依赖(requires = -1)", int(t.requires) == -1)
|
||||
Kit.check("单据九栏 + 可选两栏缺省齐备(开单面只写差异栏前提)", all_present)
|
||||
Kit.check("缺省无类型轴(attack_type 空 = 恒不查克制矩阵)", String(t.attack_type) == "")
|
||||
Kit.check("缺省无前置依赖(requires = -1)", int(t.requires) == -1)
|
||||
|
||||
|
||||
func _bare_controller():
|
||||
@@ -129,18 +113,18 @@ func _test_status_boolean_acceptance(db) -> void:
|
||||
var any_row: Dictionary = {}
|
||||
if db.status_effects.rows.size() > 0:
|
||||
any_row = db.status_effects.rows[0]
|
||||
_check("前置:状态表非空", not any_row.is_empty())
|
||||
Kit.check("前置:状态表非空", not any_row.is_empty())
|
||||
if any_row.is_empty():
|
||||
return
|
||||
var probe_id: String = String(any_row.effect_id)
|
||||
var src := {"source_class": "safe_cell", "origin_cell": Vector2i(-1, -1), "round_index": 0}
|
||||
_check("域闭授予拒绝(EC1——战斗域外 apply_effect = false)", effects.apply_effect(probe_id, src) == false)
|
||||
_check("未知 effect_id 拒绝(EC2)", effects.apply_effect("no_such_effect_id__", src) == false)
|
||||
Kit.check("域闭授予拒绝(EC1——战斗域外 apply_effect = false)", effects.apply_effect(probe_id, src) == false)
|
||||
Kit.check("未知 effect_id 拒绝(EC2)", effects.apply_effect("no_such_effect_id__", src) == false)
|
||||
effects.notify_battle_start()
|
||||
_check("开域后受理已知 id(布尔受理口径 true 通道)", effects.apply_effect(probe_id, src) == true)
|
||||
Kit.check("开域后受理已知 id(布尔受理口径 true 通道)", effects.apply_effect(probe_id, src) == true)
|
||||
var active: Array = effects.query_active(String(any_row.target_side))
|
||||
var projected := false
|
||||
for a in active:
|
||||
if String(a.effect_id) == probe_id:
|
||||
projected = int(a.effective_value) == int(a.layers) * int(a.snap_magnitude)
|
||||
_check("受理后投影 effective_value = 层数×授予时快照(抗热重载 EC9 面)", projected)
|
||||
Kit.check("受理后投影 effective_value = 层数×授予时快照(抗热重载 EC9 面)", projected)
|
||||
@@ -6,13 +6,7 @@
|
||||
extends SceneTree
|
||||
|
||||
const AuxItemsScript := preload("res://core/aux_items.gd")
|
||||
|
||||
var _problems: Array = []
|
||||
|
||||
|
||||
func check(cond: bool, msg: String) -> void:
|
||||
if not cond:
|
||||
_problems.append(msg)
|
||||
const Kit := preload("res://tests/unit/test_kit.gd") # T6 收口:check/汇总退出样板单点(静默收集通道)
|
||||
|
||||
|
||||
class StubTable:
|
||||
@@ -59,39 +53,39 @@ func _init() -> void:
|
||||
var inv = _new_inv()
|
||||
var r1: Dictionary = inv.add_item("sapper_order", "safe_cell", Vector2i(3, 4), 2)
|
||||
var r2: Dictionary = inv.add_item("nest_pendulum", "flag_hit", Vector2i(5, 6), 3)
|
||||
check(int(r1.handle) == 1 and int(r2.handle) == 2, "句柄受理序单调(1→2——零随机)")
|
||||
check(String(r1.source_class) == "safe_cell" and r1.origin_cell == Vector2i(3, 4) and int(r1.round_index) == 2,
|
||||
Kit.check_quiet(int(r1.handle) == 1 and int(r2.handle) == 2, "句柄受理序单调(1→2——零随机)")
|
||||
Kit.check_quiet(String(r1.source_class) == "safe_cell" and r1.origin_cell == Vector2i(3, 4) and int(r1.round_index) == 2,
|
||||
"授予来源可归因(source/cell/round)")
|
||||
var inv_proj: Array = inv.query_inventory(Callable())
|
||||
check(inv_proj.size() == 2 and not bool(inv_proj[0].used), "库存投影两件未用")
|
||||
check(bool(inv.consume(int(r1.handle)).get("used", false)), "consume 返回已用快照回执")
|
||||
check(inv.consume(int(r1.handle)).is_empty(), "Used 不可逆——二次 consume 拒绝(空回执)")
|
||||
check(int(inv.inventory_count()) == 2, "Used 保留只读(库存计数不变)")
|
||||
Kit.check_quiet(inv_proj.size() == 2 and not bool(inv_proj[0].used), "库存投影两件未用")
|
||||
Kit.check_quiet(bool(inv.consume(int(r1.handle)).get("used", false)), "consume 返回已用快照回执")
|
||||
Kit.check_quiet(inv.consume(int(r1.handle)).is_empty(), "Used 不可逆——二次 consume 拒绝(空回执)")
|
||||
Kit.check_quiet(int(inv.inventory_count()) == 2, "Used 保留只读(库存计数不变)")
|
||||
|
||||
# ② 真视恒可用+边缘窗口∩板(3×3 中心贴角——角 4 格)
|
||||
var v_ok: Dictionary = inv.validate_use(int(r2.handle), Vector2i(0, 0), Vector2i(10, 10), Callable())
|
||||
check(bool(v_ok.ok) and v_ok.resolved.size() == 4,
|
||||
Kit.check_quiet(bool(v_ok.ok) and v_ok.resolved.size() == 4,
|
||||
"真视边缘窗口=窗口∩板(角 4 格——护栏只缩不扩)")
|
||||
# 预读点击级零信息:中心板内但回调恒 false(全已开)→拒绝零消耗
|
||||
var inv2 = _new_inv()
|
||||
var rp: Dictionary = inv2.add_item("loot_compass", "safe_cell", Vector2i(1, 1), 1)
|
||||
var v_zero: Dictionary = inv2.validate_use(int(rp.handle), Vector2i(5, 5), Vector2i(10, 10), Callable())
|
||||
check(not bool(v_zero.ok) and int(inv2.inventory_count()) == 1,
|
||||
Kit.check_quiet(not bool(v_zero.ok) and int(inv2.inventory_count()) == 1,
|
||||
"预读点击级零信息拒绝(不消耗——库存不变)")
|
||||
# 目标越板
|
||||
var v_oob: Dictionary = inv2.validate_use(int(rp.handle), Vector2i(12, 5), Vector2i(10, 10), Callable())
|
||||
check(not bool(v_zero.ok) and not bool(v_oob.ok), "越板目标拒绝")
|
||||
Kit.check_quiet(not bool(v_zero.ok) and not bool(v_oob.ok), "越板目标拒绝")
|
||||
|
||||
# ③ 叠加层+三清(retire 不动库存;reset 全清)
|
||||
inv2.apply_overlay("mine_reveal", v_ok.resolved)
|
||||
inv2.apply_overlay("reward_preview", [Vector2i(5, 5)])
|
||||
var ov: Dictionary = inv2.query_overlays()
|
||||
check(ov.mine_reveal.size() == 4 and ov.reward_preview.size() == 1, "叠加层两集落位(真视 4/预读 1)")
|
||||
Kit.check_quiet(ov.mine_reveal.size() == 4 and ov.reward_preview.size() == 1, "叠加层两集落位(真视 4/预读 1)")
|
||||
inv2.retire_overlays()
|
||||
check(inv2.query_overlays().mine_reveal.is_empty() and int(inv2.inventory_count()) == 1,
|
||||
Kit.check_quiet(inv2.query_overlays().mine_reveal.is_empty() and int(inv2.inventory_count()) == 1,
|
||||
"换图三清:叠加层消亡+库存不动(关卡域)")
|
||||
inv2.reset()
|
||||
check(inv2.inventory_count() == 0 and inv2.probe().used == 0, "关卡末 reset 全清(幂等)")
|
||||
Kit.check_quiet(inv2.inventory_count() == 0 and inv2.probe().used == 0, "关卡末 reset 全清(幂等)")
|
||||
|
||||
# ④ 零随机重放:同操作序列两遍,投影逐位相同
|
||||
var replay_a: Array = []
|
||||
@@ -106,13 +100,7 @@ func _init() -> void:
|
||||
replay_a = [inv3.query_inventory(Callable()), inv3.query_overlays(), inv3.probe()]
|
||||
else:
|
||||
replay_b = [inv3.query_inventory(Callable()), inv3.query_overlays(), inv3.probe()]
|
||||
check(str(replay_a) == str(replay_b), "零随机重放:库存+叠加层+探针投影逐位相同")
|
||||
Kit.check_quiet(str(replay_a) == str(replay_b), "零随机重放:库存+叠加层+探针投影逐位相同")
|
||||
|
||||
# ---- 汇总 ----
|
||||
if _problems.is_empty():
|
||||
print("[aux_items_test] PASS — 授予归因/Used 不可逆/真视恒可用/零信息拒绝/边缘窗口/三清/零随机重放 全过")
|
||||
quit(0)
|
||||
else:
|
||||
for p in _problems:
|
||||
push_error("[aux_items_test] FAIL — " + p)
|
||||
quit(1)
|
||||
quit(Kit.finish("aux_items_test", "PASS — 授予归因/Used 不可逆/真视恒可用/零信息拒绝/边缘窗口/三清/零随机重放 全过"))
|
||||
@@ -8,16 +8,11 @@ extends SceneTree
|
||||
|
||||
const SaveManagerScript := preload("res://core/save_manager.gd")
|
||||
const AdventureRunnerScript := preload("res://core/adventure_runner.gd")
|
||||
const Kit := preload("res://tests/unit/test_kit.gd") # T6 收口:check/汇总退出样板单点(静默收集通道)
|
||||
|
||||
var _problems: Array = []
|
||||
var _bp_path := "user://test_breakpoint_resume.json"
|
||||
|
||||
|
||||
func check(cond: bool, msg: String) -> void:
|
||||
if not cond:
|
||||
_problems.append(msg)
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
var db = load("res://core/config_db.gd").new()
|
||||
if not db.load_tables("res://data/tables", false):
|
||||
@@ -32,64 +27,64 @@ func _init() -> void:
|
||||
var adr_a = AdventureRunnerScript.new(db, rng_a)
|
||||
adr_a.set_save_manager(sm_a)
|
||||
var params := {"hero_override": {}, "aux_unlocked": {}}
|
||||
check(adr_a.start_adventure(1234, params), "冒险启动(入口停点)")
|
||||
check(sm_a.has_breakpoint(), "入口停点断点在档")
|
||||
check(int((sm_a.breakpoint_snapshot() as Dictionary).get("level_index", -1)) == 1, "断点关卡序号 = 1")
|
||||
check(adr_a.abandon(), "入口停点放弃受理(非战斗期)")
|
||||
check(not sm_a.has_breakpoint() and sm_a.write_to_disk(), "放弃清断点+写盘(退出=清除断点)")
|
||||
Kit.check_quiet(adr_a.start_adventure(1234, params), "冒险启动(入口停点)")
|
||||
Kit.check_quiet(sm_a.has_breakpoint(), "入口停点断点在档")
|
||||
Kit.check_quiet(int((sm_a.breakpoint_snapshot() as Dictionary).get("level_index", -1)) == 1, "断点关卡序号 = 1")
|
||||
Kit.check_quiet(adr_a.abandon(), "入口停点放弃受理(非战斗期)")
|
||||
Kit.check_quiet(not sm_a.has_breakpoint() and sm_a.write_to_disk(), "放弃清断点+写盘(退出=清除断点)")
|
||||
var sm_reload = SaveManagerScript.new(_bp_path)
|
||||
sm_reload.load_from_disk()
|
||||
check(not sm_reload.has_breakpoint(), "放弃后重读档无断点(「继续上局」隐藏口径)")
|
||||
Kit.check_quiet(not sm_reload.has_breakpoint(), "放弃后重读档无断点(「继续上局」隐藏口径)")
|
||||
|
||||
# ---- Part 2:中断基线——入口停点后选路进战,记板指纹与计数器 ----
|
||||
var rng_b = load("res://core/rng_service.gd").new()
|
||||
var sm_b = SaveManagerScript.new(_bp_path)
|
||||
var adr_b = AdventureRunnerScript.new(db, rng_b)
|
||||
adr_b.set_save_manager(sm_b)
|
||||
check(adr_b.start_adventure(1234, params), "基线冒险启动(同种子 1234)")
|
||||
Kit.check_quiet(adr_b.start_adventure(1234, params), "基线冒险启动(同种子 1234)")
|
||||
var runner_b = adr_b.runner()
|
||||
var c0: int = rng_b.counter()
|
||||
var cand_b: Array = runner_b.next_candidate_ids()
|
||||
var hp0: int = runner_b.hero_hp
|
||||
var bp_disk: Dictionary = sm_b.breakpoint_snapshot()
|
||||
check(int(bp_disk.get("rng_counter", -1)) == c0, "断点计数器 = 当前取数号位(快照契约)")
|
||||
check(runner_b.pick_next_node(int(cand_b[0])), "基线选路进首战")
|
||||
Kit.check_quiet(int(bp_disk.get("rng_counter", -1)) == c0, "断点计数器 = 当前取数号位(快照契约)")
|
||||
Kit.check_quiet(runner_b.pick_next_node(int(cand_b[0])), "基线选路进首战")
|
||||
var battle_b = runner_b.current_battle()
|
||||
check(battle_b != null, "基线首战开打")
|
||||
Kit.check_quiet(battle_b != null, "基线首战开打")
|
||||
var fp_b := _board_fingerprint(battle_b.minesweeper.current_board())
|
||||
var c1: int = rng_b.counter()
|
||||
|
||||
# ---- Part 3:续玩确定性——新进程对象自盘上断点重建,同种子续玩板逐位一致 ----
|
||||
var sm_c = SaveManagerScript.new(_bp_path)
|
||||
check(sm_c.load_from_disk() and sm_c.has_breakpoint(), "断点自盘读回在档")
|
||||
Kit.check_quiet(sm_c.load_from_disk() and sm_c.has_breakpoint(), "断点自盘读回在档")
|
||||
var bp_c: Dictionary = sm_c.breakpoint_snapshot()
|
||||
var rng_c = load("res://core/rng_service.gd").new()
|
||||
var adr_c = AdventureRunnerScript.new(db, rng_c)
|
||||
check(adr_c.resume_adventure(bp_c, params), "续玩受理")
|
||||
Kit.check_quiet(adr_c.resume_adventure(bp_c, params), "续玩受理")
|
||||
var runner_c = adr_c.runner()
|
||||
check(int(adr_c.master_seed) == 1234, "续玩主种子回填")
|
||||
check(int(rng_c.counter()) == c0, "续玩取数号位 = 断点号位(零重耗零跳号)")
|
||||
check(runner_c.hero_hp == hp0, "续玩生命账本回填")
|
||||
Kit.check_quiet(int(adr_c.master_seed) == 1234, "续玩主种子回填")
|
||||
Kit.check_quiet(int(rng_c.counter()) == c0, "续玩取数号位 = 断点号位(零重耗零跳号)")
|
||||
Kit.check_quiet(runner_c.hero_hp == hp0, "续玩生命账本回填")
|
||||
var cand_c: Array = runner_c.next_candidate_ids()
|
||||
check(cand_c.size() == cand_b.size() and int(cand_c[0]) == int(cand_b[0]), "续玩候选一致(停点重建)")
|
||||
check(runner_c.awaiting_choice(), "续玩处选路停点态")
|
||||
check(runner_c.pick_next_node(int(cand_c[0])), "续玩选路进首战")
|
||||
Kit.check_quiet(cand_c.size() == cand_b.size() and int(cand_c[0]) == int(cand_b[0]), "续玩候选一致(停点重建)")
|
||||
Kit.check_quiet(runner_c.awaiting_choice(), "续玩处选路停点态")
|
||||
Kit.check_quiet(runner_c.pick_next_node(int(cand_c[0])), "续玩选路进首战")
|
||||
var battle_c = runner_c.current_battle()
|
||||
var fp_c := _board_fingerprint(battle_c.minesweeper.current_board())
|
||||
check(fp_c == fp_b, "续玩首战棋盘与中断前时间线逐位一致(同种子同板)")
|
||||
check(int(rng_c.counter()) == c1, "续玩开板后号位与基线一致")
|
||||
Kit.check_quiet(fp_c == fp_b, "续玩首战棋盘与中断前时间线逐位一致(同种子同板)")
|
||||
Kit.check_quiet(int(rng_c.counter()) == c1, "续玩开板后号位与基线一致")
|
||||
|
||||
# ---- Part 4:续玩幂等——同一断点二次续玩(新进程对象)结果逐位同 ----
|
||||
var sm_d = SaveManagerScript.new(_bp_path)
|
||||
sm_d.load_from_disk()
|
||||
var rng_d = load("res://core/rng_service.gd").new()
|
||||
var adr_d = AdventureRunnerScript.new(db, rng_d)
|
||||
check(adr_d.resume_adventure(sm_d.breakpoint_snapshot(), params), "二次续玩受理")
|
||||
Kit.check_quiet(adr_d.resume_adventure(sm_d.breakpoint_snapshot(), params), "二次续玩受理")
|
||||
var runner_d = adr_d.runner()
|
||||
check(int(rng_d.counter()) == c0 and runner_d.hero_hp == hp0, "二次续玩状态逐位同(幂等)")
|
||||
check(runner_d.pick_next_node(int(runner_d.next_candidate_ids()[0])), "二次续玩选路")
|
||||
Kit.check_quiet(int(rng_d.counter()) == c0 and runner_d.hero_hp == hp0, "二次续玩状态逐位同(幂等)")
|
||||
Kit.check_quiet(runner_d.pick_next_node(int(runner_d.next_candidate_ids()[0])), "二次续玩选路")
|
||||
var battle_d = runner_d.current_battle()
|
||||
check(_board_fingerprint(battle_d.minesweeper.current_board()) == fp_b, "二次续玩板逐位同(幂等)")
|
||||
Kit.check_quiet(_board_fingerprint(battle_d.minesweeper.current_board()) == fp_b, "二次续玩板逐位同(幂等)")
|
||||
|
||||
# ---- Part 5:三选一停点中断→续玩(迭代 10 奖励重构——候选不重抽、已兑付不重付、次数计数一致)----
|
||||
var rng_e = load("res://core/rng_service.gd").new()
|
||||
@@ -97,50 +92,43 @@ func _init() -> void:
|
||||
var adr_e = AdventureRunnerScript.new(db, rng_e)
|
||||
adr_e.set_save_manager(sm_e)
|
||||
var params_e := {"hero_override": {}, "aux_unlocked": {"blast_kit": true}}
|
||||
check(adr_e.start_adventure(4321, params_e), "P5 冒险启动(seed 4321)")
|
||||
Kit.check_quiet(adr_e.start_adventure(4321, params_e), "P5 冒险启动(seed 4321)")
|
||||
adr_e._bonus_taken["attack"] = 1 # 白盒预置计数(断点附随件往返的判据面——计数不可由加算区反推)
|
||||
adr_e._on_level_finished({"result": "victory", "hp": 30, "node_index": 1, "node_id": 1, "node_type": "shop"})
|
||||
var cands_e: Array = (adr_e._bonus_pending.get("candidates", []) as Array).duplicate()
|
||||
check(not cands_e.is_empty(), "P5 非末关通关三选一停点在案(直调 victory——L1 非末关)")
|
||||
Kit.check_quiet(not cands_e.is_empty(), "P5 非末关通关三选一停点在案(直调 victory——L1 非末关)")
|
||||
var sm_f = SaveManagerScript.new(_bp_path)
|
||||
check(sm_f.load_from_disk() and sm_f.has_breakpoint(), "P5 选择停点断点在档(选择停点可中断契约)")
|
||||
Kit.check_quiet(sm_f.load_from_disk() and sm_f.has_breakpoint(), "P5 选择停点断点在档(选择停点可中断契约)")
|
||||
var bp_f: Dictionary = sm_f.breakpoint_snapshot()
|
||||
var bp_f_cands: Array = (bp_f.get("bonus_pending", {}) as Dictionary).get("candidates", []) as Array
|
||||
check(bp_f_cands == cands_e, "P5 断点附随件含候选(bonus_pending 往返)")
|
||||
check(int((bp_f.get("bonus_taken", {}) as Dictionary).get("attack", -1)) == 1, "P5 断点附随件含次数计数(bonus_taken 往返)")
|
||||
Kit.check_quiet(bp_f_cands == cands_e, "P5 断点附随件含候选(bonus_pending 往返)")
|
||||
Kit.check_quiet(int((bp_f.get("bonus_taken", {}) as Dictionary).get("attack", -1)) == 1, "P5 断点附随件含次数计数(bonus_taken 往返)")
|
||||
var got_cands := {"cands": [] as Array}
|
||||
var adr_f = AdventureRunnerScript.new(db, load("res://core/rng_service.gd").new())
|
||||
adr_f.set_save_manager(sm_f) # 兑付后停点快照落盘承载(与真机组合根同构)
|
||||
adr_f.bonus_choice_presented.connect(func(p): got_cands["cands"] = (p.get("candidates", []) as Array).duplicate())
|
||||
check(adr_f.resume_adventure(bp_f, params_e), "P5 续玩受理(选择态分支)")
|
||||
check(adr_f.runner() == null, "P5 续玩不重跑关卡(三选一停点直回,_start_current_level 未走)")
|
||||
check(got_cands["cands"] == cands_e, "P5 续玩候选不重抽(逐位一致)")
|
||||
check(int(adr_f._bonus_taken["attack"]) == 1, "P5 次数计数恢复一致(牌库剔除态续接)")
|
||||
check(not adr_f.continue_to_next_level(), "P5 未兑付推进拒绝(续玩态零自动推进同契约)")
|
||||
check(adr_f.pick_bonus(String(cands_e[0])), "P5 点选兑付受理")
|
||||
Kit.check_quiet(adr_f.resume_adventure(bp_f, params_e), "P5 续玩受理(选择态分支)")
|
||||
Kit.check_quiet(adr_f.runner() == null, "P5 续玩不重跑关卡(三选一停点直回,_start_current_level 未走)")
|
||||
Kit.check_quiet(got_cands["cands"] == cands_e, "P5 续玩候选不重抽(逐位一致)")
|
||||
Kit.check_quiet(int(adr_f._bonus_taken["attack"]) == 1, "P5 次数计数恢复一致(牌库剔除态续接)")
|
||||
Kit.check_quiet(not adr_f.continue_to_next_level(), "P5 未兑付推进拒绝(续玩态零自动推进同契约)")
|
||||
Kit.check_quiet(adr_f.pick_bonus(String(cands_e[0])), "P5 点选兑付受理")
|
||||
var picked_e: String = String(adr_f._bonus_result.get("picked", ""))
|
||||
if adr_f._bonus_taken.has(picked_e):
|
||||
check(int(adr_f._bonus_taken[picked_e]) == (2 if picked_e == "attack" else 1),
|
||||
Kit.check_quiet(int(adr_f._bonus_taken[picked_e]) == (2 if picked_e == "attack" else 1),
|
||||
"P5 兑付计数一致(%s——预置 1 + 本次兑付)" % picked_e)
|
||||
check(sm_f.has_breakpoint(), "P5 兑付后停点快照在档(已兑付待推进态可中断)")
|
||||
Kit.check_quiet(sm_f.has_breakpoint(), "P5 兑付后停点快照在档(已兑付待推进态可中断)")
|
||||
var bp_g: Dictionary = sm_f.breakpoint_snapshot()
|
||||
var adr_g = AdventureRunnerScript.new(db, load("res://core/rng_service.gd").new())
|
||||
adr_g.set_save_manager(sm_f) # 已兑付态续玩的后续快照同承载
|
||||
var got_result := {"r": {}}
|
||||
adr_g.bonus_resolved.connect(func(r): got_result["r"] = r.duplicate())
|
||||
check(adr_g.resume_adventure(bp_g, params_e), "P5 已兑付态续玩受理")
|
||||
check(String(got_result["r"].get("picked", "")) == picked_e, "P5 已兑付不重付(resolved 重发同件)")
|
||||
check(adr_g.continue_to_next_level(), "P5 已兑付续玩后推进受理(下一关开始)")
|
||||
Kit.check_quiet(adr_g.resume_adventure(bp_g, params_e), "P5 已兑付态续玩受理")
|
||||
Kit.check_quiet(String(got_result["r"].get("picked", "")) == picked_e, "P5 已兑付不重付(resolved 重发同件)")
|
||||
Kit.check_quiet(adr_g.continue_to_next_level(), "P5 已兑付续玩后推进受理(下一关开始)")
|
||||
|
||||
_clear_disk()
|
||||
if _problems.is_empty():
|
||||
print("[breakpoint_resume_test] PASS — 断点往返/续玩确定性/幂等/退出清断点 全绿")
|
||||
quit(0)
|
||||
else:
|
||||
for p in _problems:
|
||||
push_error("[breakpoint_resume_test] FAIL — " + p)
|
||||
print("[breakpoint_resume_test] FAIL — " + p)
|
||||
quit(1)
|
||||
quit(Kit.finish("breakpoint_resume_test", "PASS — 断点往返/续玩确定性/幂等/退出清断点 全绿"))
|
||||
|
||||
|
||||
func _board_fingerprint(b) -> String:
|
||||
|
||||
@@ -7,27 +7,12 @@
|
||||
extends SceneTree
|
||||
|
||||
const ConfigDBScript := preload("res://core/config_db.gd")
|
||||
const Kit := preload("res://tests/unit/test_kit.gd") # T6 收口:BAD 表目录夹具共享(copy_tables)
|
||||
|
||||
const SRC_DIR := "res://data/tables"
|
||||
const BAD_DIR := "user://aux_fixture_target_violation"
|
||||
|
||||
|
||||
func _copy_tables() -> bool:
|
||||
DirAccess.make_dir_recursive_absolute(BAD_DIR)
|
||||
var dir := DirAccess.open(SRC_DIR)
|
||||
if dir == null:
|
||||
return false
|
||||
dir.list_dir_begin()
|
||||
var f := dir.get_next()
|
||||
while f != "":
|
||||
if not dir.current_is_dir():
|
||||
var err := dir.copy(SRC_DIR + "/" + f, BAD_DIR + "/" + f)
|
||||
if err != OK:
|
||||
return false
|
||||
f = dir.get_next()
|
||||
return true
|
||||
|
||||
|
||||
func _inject_violation() -> bool:
|
||||
var path := BAD_DIR + "/aux_items.tres"
|
||||
var f := FileAccess.open(path, FileAccess.READ)
|
||||
@@ -58,7 +43,7 @@ func _inject_violation() -> bool:
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
if not _copy_tables() or not _inject_violation():
|
||||
if not Kit.copy_tables(SRC_DIR, BAD_DIR) or not _inject_violation():
|
||||
push_error("[aux_target_violation_test] 夹具目录准备失败")
|
||||
quit(2)
|
||||
return
|
||||
|
||||
@@ -5,16 +5,14 @@
|
||||
extends SceneTree
|
||||
|
||||
const ConfigDBScript := preload("res://core/config_db.gd")
|
||||
|
||||
var _failures := 0
|
||||
var _checks := 0
|
||||
const Kit := preload("res://tests/unit/test_kit.gd") # T6 收口:_check/汇总退出样板单点
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
var db = ConfigDBScript.new()
|
||||
var load_ok: bool = db.load_tables("res://data/tables", true)
|
||||
_check("全部表加载 + 校验通过(含跨表护栏零 error 诊断)", load_ok and db.diagnostics_count("error") == 0)
|
||||
_check("is_ready 通道随加载置位", db.is_ready)
|
||||
Kit.check("全部表加载 + 校验通过(含跨表护栏零 error 诊断)", load_ok and db.diagnostics_count("error") == 0)
|
||||
Kit.check("is_ready 通道随加载置位", db.is_ready)
|
||||
if not load_ok:
|
||||
for d in db.diagnostics():
|
||||
if d.severity == "error":
|
||||
@@ -25,21 +23,7 @@ func _init() -> void:
|
||||
_test_shop_accessors(db)
|
||||
_test_meta_accessors(db)
|
||||
_test_manifest_dir_sync(db)
|
||||
if _failures > 0:
|
||||
push_error("[config_db_test] %d/%d 项失败" % [_failures, _checks])
|
||||
quit(1)
|
||||
return
|
||||
print("[config_db_test] all green (%d checks)" % _checks)
|
||||
quit(0)
|
||||
|
||||
|
||||
func _check(desc: String, ok: bool) -> void:
|
||||
_checks += 1
|
||||
if ok:
|
||||
print(" [PASS] " + desc)
|
||||
else:
|
||||
_failures += 1
|
||||
push_error(" [FAIL] " + desc)
|
||||
quit(Kit.finish("config_db_test"))
|
||||
|
||||
|
||||
# ---- 进度坐标 ceil 解析(R1:向上下取整保证偏难;越最大行 clamp 取最大值 + 警告去重)----
|
||||
@@ -67,34 +51,34 @@ func _test_progress_resolution(db) -> void:
|
||||
# 精确命中:请求 = 表内现存 (L,D) → 原行返回(字段逐位相等)
|
||||
var probe_row: Dictionary = bg_rows[0]
|
||||
var got: Dictionary = db.resolve_board_gen(int(probe_row.level_index), int(probe_row.node_depth))
|
||||
_check("进度解析精确命中返回原行(board_gen 首行)",
|
||||
Kit.check("进度解析精确命中返回原行(board_gen 首行)",
|
||||
int(got.get("level_index", -99)) == int(probe_row.level_index)
|
||||
and int(got.get("node_depth", -99)) == int(probe_row.node_depth)
|
||||
and String(got.get("board_type", "")) == String(probe_row.get("board_type", "")))
|
||||
|
||||
# enemy 行集同通道:同 (L,D) 精确行存在(关卡节点地图 前置——battle 节点双表精确行契约)
|
||||
var en_got: Dictionary = db.resolve_enemy_rowset(int(probe_row.level_index), int(probe_row.node_depth))
|
||||
_check("enemy 行集同坐标解析非空(双表精确行契约)", not en_got.is_empty())
|
||||
Kit.check("enemy 行集同坐标解析非空(双表精确行契约)", not en_got.is_empty())
|
||||
|
||||
# 深度 clamp:请求深度超最大行 → 取该关最大深度行 + 警告诊断一次(去重)
|
||||
db.reset_diagnostics()
|
||||
var deep: Dictionary = db.resolve_board_gen(min_l, int(min_depths[-1]) + 1)
|
||||
_check("深度越最大行 clamp 取该关最大深度行",
|
||||
Kit.check("深度越最大行 clamp 取该关最大深度行",
|
||||
int(deep.get("level_index", -99)) == min_l and int(deep.get("node_depth", -99)) == int(min_depths[-1]))
|
||||
_check("clamp 解析发警告诊断(resolve_depth_clamp)", db.diagnostics_count("warning") >= 1)
|
||||
Kit.check("clamp 解析发警告诊断(resolve_depth_clamp)", db.diagnostics_count("warning") >= 1)
|
||||
var found_clamp := false
|
||||
for d in db.diagnostics():
|
||||
if d.category == "resolve_depth_clamp":
|
||||
found_clamp = true
|
||||
_check("警告类别 = resolve_depth_clamp", found_clamp)
|
||||
Kit.check("警告类别 = resolve_depth_clamp", found_clamp)
|
||||
var warn_before: int = db.diagnostics_count("warning")
|
||||
db.resolve_board_gen(min_l, int(min_depths[-1]) + 1)
|
||||
_check("同键重复请求警告去重(四元组注册表)", db.diagnostics_count("warning") == warn_before)
|
||||
Kit.check("同键重复请求警告去重(四元组注册表)", db.diagnostics_count("warning") == warn_before)
|
||||
|
||||
# 关卡 clamp:请求关卡超最大行 → 取最大关卡行
|
||||
db.reset_diagnostics()
|
||||
var high: Dictionary = db.resolve_board_gen(max_l + 1, 0)
|
||||
_check("关卡越最大行 clamp 取最大关卡行", int(high.get("level_index", -99)) == max_l)
|
||||
Kit.check("关卡越最大行 clamp 取最大关卡行", int(high.get("level_index", -99)) == max_l)
|
||||
|
||||
|
||||
# ---- 变异池门控(unlock_level 闸 + ∅ board_types 通配)----
|
||||
@@ -114,12 +98,12 @@ func _test_variant_pool_gate(db) -> void:
|
||||
var tag: String = String(gated_row.board_types[0])
|
||||
var before: Array = db.variant_pool(u - 1, tag)
|
||||
var after: Array = db.variant_pool(u, tag)
|
||||
_check("变异池 unlock 前不含未解锁行(level=unlock−1)", not _rows_contain(before, gated_row))
|
||||
_check("变异池 unlock 当级含该行", _rows_contain(after, gated_row))
|
||||
Kit.check("变异池 unlock 前不含未解锁行(level=unlock−1)", not _rows_contain(before, gated_row))
|
||||
Kit.check("变异池 unlock 当级含该行", _rows_contain(after, gated_row))
|
||||
if not wildcard_row.is_empty():
|
||||
var u2: int = int(wildcard_row.unlock_level)
|
||||
var any_pool: Array = db.variant_pool(u2, "nonexistent_board_type_tag")
|
||||
_check("∅ board_types 行通配(任一 board_type 命中)", _rows_contain(any_pool, wildcard_row))
|
||||
Kit.check("∅ board_types 行通配(任一 board_type 命中)", _rows_contain(any_pool, wildcard_row))
|
||||
|
||||
|
||||
func _rows_contain(pool: Array, row: Dictionary) -> bool:
|
||||
@@ -134,26 +118,26 @@ func _rows_contain(pool: Array, row: Dictionary) -> bool:
|
||||
func _test_shop_accessors(db) -> void:
|
||||
var products: Array = db.shop_products()
|
||||
if products.is_empty():
|
||||
_check("ShopTable products 行族非空(表已落籍前提)", false)
|
||||
Kit.check("ShopTable products 行族非空(表已落籍前提)", false)
|
||||
return
|
||||
_check("ShopTable products 行族非空", true)
|
||||
Kit.check("ShopTable products 行族非空", true)
|
||||
var first: Dictionary = products[0]
|
||||
var got: Dictionary = db.shop_product_row(String(first.product_id))
|
||||
_check("shop_product_row 按 product_id 精确命中首行", got == first)
|
||||
_check("shop_product_row 未知 id 返回空字典(容错通道)", db.shop_product_row("no_such_product_id__").is_empty())
|
||||
Kit.check("shop_product_row 按 product_id 精确命中首行", got == first)
|
||||
Kit.check("shop_product_row 未知 id 返回空字典(容错通道)", db.shop_product_row("no_such_product_id__").is_empty())
|
||||
var tier_rows: Array = db._shop_rows("gp_drops")
|
||||
if not tier_rows.is_empty():
|
||||
var tier0: String = String(tier_rows[0].tier)
|
||||
_check("shop_gp_drop_row 按 tier 命中", not db.shop_gp_drop_row(tier0).is_empty())
|
||||
Kit.check("shop_gp_drop_row 按 tier 命中", not db.shop_gp_drop_row(tier0).is_empty())
|
||||
var bonus_rows: Array = db._shop_rows("level_gp_bonus")
|
||||
if not bonus_rows.is_empty():
|
||||
var blv: int = int(bonus_rows[0].level_index)
|
||||
_check("shop_level_gp_bonus 按关卡序号命中行值", db.shop_level_gp_bonus(blv) == int(bonus_rows[0].bonus))
|
||||
_check("shop_level_gp_bonus 未登记关卡返回 0(兜底通道)", db.shop_level_gp_bonus(-1) == 0)
|
||||
Kit.check("shop_level_gp_bonus 按关卡序号命中行值", db.shop_level_gp_bonus(blv) == int(bonus_rows[0].bonus))
|
||||
Kit.check("shop_level_gp_bonus 未登记关卡返回 0(兜底通道)", db.shop_level_gp_bonus(-1) == 0)
|
||||
var price_rows: Array = db._shop_rows("price_level_bonus")
|
||||
if not price_rows.is_empty():
|
||||
var plv: int = int(price_rows[0].level_index)
|
||||
_check("shop_price_level_bonus 按关卡序号命中行值", db.shop_price_level_bonus(plv) == int(price_rows[0].bonus))
|
||||
Kit.check("shop_price_level_bonus 按关卡序号命中行值", db.shop_price_level_bonus(plv) == int(price_rows[0].bonus))
|
||||
|
||||
|
||||
# ---- MetaTable 消费面 ----
|
||||
@@ -161,19 +145,19 @@ func _test_shop_accessors(db) -> void:
|
||||
func _test_meta_accessors(db) -> void:
|
||||
var forge_rows: Array = db._meta_rows("forge_products")
|
||||
if forge_rows.is_empty():
|
||||
_check("MetaTable forge_products 行族非空", false)
|
||||
Kit.check("MetaTable forge_products 行族非空", false)
|
||||
else:
|
||||
var attr0: String = String(forge_rows[0].attr)
|
||||
_check("meta_forge_row 按 attr 命中首行", db.meta_forge_row(attr0) == forge_rows[0])
|
||||
_check("meta_forge_row 未知 attr 返回空字典", db.meta_forge_row("no_such_attr__").is_empty())
|
||||
Kit.check("meta_forge_row 按 attr 命中首行", db.meta_forge_row(attr0) == forge_rows[0])
|
||||
Kit.check("meta_forge_row 未知 attr 返回空字典", db.meta_forge_row("no_such_attr__").is_empty())
|
||||
var key_rows: Array = db._meta_rows("boss_keystone")
|
||||
if not key_rows.is_empty():
|
||||
var klv: int = int(key_rows[0].level_index)
|
||||
_check("meta_boss_keystone_row 按关卡序号命中", db.meta_boss_keystone_row(klv) == key_rows[0])
|
||||
Kit.check("meta_boss_keystone_row 按关卡序号命中", db.meta_boss_keystone_row(klv) == key_rows[0])
|
||||
var conv_rows: Array = db._meta_rows("conversion")
|
||||
if not conv_rows.is_empty():
|
||||
var cattr: String = String(conv_rows[0].attr)
|
||||
_check("meta_conversion_row 按 attr 命中", db.meta_conversion_row(cattr) == conv_rows[0])
|
||||
Kit.check("meta_conversion_row 按 attr 命中", db.meta_conversion_row(cattr) == conv_rows[0])
|
||||
|
||||
|
||||
# ---- TABLE_MANIFEST ↔ data/tables 目录同步(各系统→D2 单点契约的登记面)----
|
||||
@@ -195,4 +179,4 @@ func _test_manifest_dir_sync(db) -> void:
|
||||
for key in manifest.keys():
|
||||
if not disk_files.has(String(manifest[key])):
|
||||
synced = false
|
||||
_check("TABLE_MANIFEST 与 data/tables 目录逐文件同步(无缺登/无悬登)", synced)
|
||||
Kit.check("TABLE_MANIFEST 与 data/tables 目录逐文件同步(无缺登/无悬登)", synced)
|
||||
@@ -7,26 +7,12 @@
|
||||
extends SceneTree
|
||||
|
||||
const ConfigDBScript := preload("res://core/config_db.gd")
|
||||
const Kit := preload("res://tests/unit/test_kit.gd") # T6 收口:BAD 表目录夹具共享(copy_tables)
|
||||
|
||||
const SRC_DIR := "res://data/tables"
|
||||
const BAD_DIR := "user://aux_fixture_dangling_aux"
|
||||
|
||||
|
||||
func _copy_tables() -> bool:
|
||||
DirAccess.make_dir_recursive_absolute(BAD_DIR)
|
||||
var dir := DirAccess.open(SRC_DIR)
|
||||
if dir == null:
|
||||
return false
|
||||
dir.list_dir_begin()
|
||||
var f := dir.get_next()
|
||||
while f != "":
|
||||
if not dir.current_is_dir():
|
||||
if dir.copy(SRC_DIR + "/" + f, BAD_DIR + "/" + f) != OK:
|
||||
return false
|
||||
f = dir.get_next()
|
||||
return true
|
||||
|
||||
|
||||
func _inject_violation() -> bool:
|
||||
var path := BAD_DIR + "/reward.tres"
|
||||
var f := FileAccess.open(path, FileAccess.READ)
|
||||
@@ -50,7 +36,7 @@ func _inject_violation() -> bool:
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
if not _copy_tables() or not _inject_violation():
|
||||
if not Kit.copy_tables(SRC_DIR, BAD_DIR) or not _inject_violation():
|
||||
push_error("[reward_dangling_aux_test] 夹具目录准备失败")
|
||||
quit(2)
|
||||
return
|
||||
|
||||
@@ -6,33 +6,17 @@
|
||||
extends SceneTree
|
||||
|
||||
const RngServiceScript := preload("res://core/rng_service.gd")
|
||||
const Kit := preload("res://tests/unit/test_kit.gd") # T6 收口:_check/汇总退出样板单点
|
||||
|
||||
const TEST_SEED := 20260928 # 夹具种子(任意固定值;语义 = 可复现性,非玩法数值)
|
||||
|
||||
var _failures := 0
|
||||
var _checks := 0
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
_test_derive_determinism()
|
||||
_test_counter_ledger()
|
||||
_test_stream_draw_determinism()
|
||||
_test_negative_seed_normalization()
|
||||
if _failures > 0:
|
||||
push_error("[rng_service_test] %d/%d 项失败" % [_failures, _checks])
|
||||
quit(1)
|
||||
return
|
||||
print("[rng_service_test] all green (%d checks)" % _checks)
|
||||
quit(0)
|
||||
|
||||
|
||||
func _check(desc: String, ok: bool) -> void:
|
||||
_checks += 1
|
||||
if ok:
|
||||
print(" [PASS] " + desc)
|
||||
else:
|
||||
_failures += 1
|
||||
push_error(" [FAIL] " + desc)
|
||||
quit(Kit.finish("rng_service_test"))
|
||||
|
||||
|
||||
func _fresh_service():
|
||||
@@ -49,11 +33,11 @@ func _test_derive_determinism() -> void:
|
||||
for scope in range(8):
|
||||
if a.derive_at(scope) != b.derive_at(scope):
|
||||
same = false
|
||||
_check("同种子两实例 derive_at(0..7) 逐位一致", same)
|
||||
Kit.check("同种子两实例 derive_at(0..7) 逐位一致", same)
|
||||
var a2 = _fresh_service()
|
||||
var before: int = a2.counter()
|
||||
a2.derive_at(0)
|
||||
_check("derive_at 为纯函数不推计数器", a2.counter() == before)
|
||||
Kit.check("derive_at 为纯函数不推计数器", a2.counter() == before)
|
||||
|
||||
|
||||
# 全局取数计数器:唯一账本——open_stream 逐次 +1(scope 单调)
|
||||
@@ -64,13 +48,13 @@ func _test_counter_ledger() -> void:
|
||||
var c1: int = rng.counter()
|
||||
rng.open_stream("armor_roll")
|
||||
var c2: int = rng.counter()
|
||||
_check("open_stream 逐次推进全局计数器(+1/次)", c1 == c0 + 1 and c2 == c0 + 2)
|
||||
Kit.check("open_stream 逐次推进全局计数器(+1/次)", c1 == c0 + 1 and c2 == c0 + 2)
|
||||
# 用途标签只进事件日志不参与派生——同 scope 同派生种子(ADR-0007 裁丙口径)
|
||||
var rng_x = _fresh_service()
|
||||
var rng_y = _fresh_service()
|
||||
var s1: int = int(rng_x.open_stream("board_gen").seed)
|
||||
var s2: int = int(rng_y.open_stream("reward_roll").seed)
|
||||
_check("同 scope 异用途标签派生种子一致(标签不参与派生)", s1 == s2)
|
||||
Kit.check("同 scope 异用途标签派生种子一致(标签不参与派生)", s1 == s2)
|
||||
|
||||
|
||||
# 同种子两实例同标签流的实际取数序列逐位一致(回放确定性契约——存档快照=主种子+计数器的前提)
|
||||
@@ -84,7 +68,7 @@ func _test_stream_draw_determinism() -> void:
|
||||
var rb: int = b.open_stream("armor_roll").randi_range(1, faces)
|
||||
if ra != rb:
|
||||
same = false
|
||||
_check("同种子两实例同标签流取数 12 次逐位一致", same)
|
||||
Kit.check("同种子两实例同标签流取数 12 次逐位一致", same)
|
||||
|
||||
|
||||
# 负输入 abs 归一化(警告通道)——|−s| 与 s 同派生
|
||||
@@ -92,4 +76,4 @@ func _test_negative_seed_normalization() -> void:
|
||||
var pos = _fresh_service()
|
||||
var neg = RngServiceScript.new()
|
||||
neg.set_master_seed(-TEST_SEED)
|
||||
_check("负种子 abs 归一化后派生与正种子一致", pos.derive_at(0) == neg.derive_at(0))
|
||||
Kit.check("负种子 abs 归一化后派生与正种子一致", pos.derive_at(0) == neg.derive_at(0))
|
||||
@@ -7,23 +7,12 @@
|
||||
# 第一个真实测试:复制本文件为 tests/unit/<system>/<system>_<feature>_test.gd 并替换占位断言
|
||||
extends SceneTree
|
||||
|
||||
func _init() -> void:
|
||||
var failures := 0
|
||||
failures += _check("headless SceneTree 协议可用(quit(code) 通道)", _test_headless_protocol())
|
||||
failures += _check("命名规范可执行:test_[scenario]_[expected]", _test_naming_convention_followed())
|
||||
if failures > 0:
|
||||
push_error("[example_test] %d 项失败" % failures)
|
||||
quit(1)
|
||||
return
|
||||
print("[example_test] all green")
|
||||
quit(0)
|
||||
const Kit := preload("res://tests/unit/test_kit.gd") # T6 收口:check/汇总退出样板单点
|
||||
|
||||
func _check(desc: String, ok: bool) -> int:
|
||||
if not ok:
|
||||
push_error("[example_test] FAIL: " + desc)
|
||||
return 1
|
||||
print("[example_test] PASS: " + desc)
|
||||
return 0
|
||||
func _init() -> void:
|
||||
Kit.check("headless SceneTree 协议可用(quit(code) 通道)", _test_headless_protocol())
|
||||
Kit.check("命名规范可执行:test_[scenario]_[expected]", _test_naming_convention_followed())
|
||||
quit(Kit.finish("example_test"))
|
||||
|
||||
func _test_headless_protocol() -> bool:
|
||||
# 占位断言:验证脚本本身在 SceneTree 上下文可执行。
|
||||
|
||||
@@ -3,6 +3,17 @@ extends SceneTree
|
||||
# 原守 node_overlay.show_reward_items;奖励节点三选一与通关三选一合并共用 BonusPickOverlay 后,
|
||||
# 本套件改守共用面的「功能存在 ≠ 功能可达」盲区:选择外发/强制确认两段式/替换子面板两路/结果态。
|
||||
# 运行:godot --headless --path game --script res://tests/unit/presentation/reward_overlay_test.gd(expect 0)
|
||||
# T6 收口批:断言走 test_kit 计数(Kit.check)+ 失败即停早退协议保留(顺序状态机——坏态下后继断言无效)。
|
||||
const Kit := preload("res://tests/unit/test_kit.gd")
|
||||
|
||||
# 断言 + 失败即停(原直写 print FAIL + quit(1) + return 样板单点化——早退语义逐位保持)
|
||||
func _req(desc: String, ok: bool) -> bool:
|
||||
Kit.check(desc, ok)
|
||||
if not ok:
|
||||
quit(1)
|
||||
return false
|
||||
return true
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
@@ -25,17 +36,15 @@ func _run() -> void:
|
||||
for c in ov._choice_box.get_children():
|
||||
if c is Button:
|
||||
btns += 1
|
||||
if ov.visible != true or btns != 3 or ov._confirm_btn.visible or ov._continue_btn.visible:
|
||||
print("[reward_verify] FAIL — 选择态应三卡呈现且确认/继续隐藏(btns=%d)" % btns)
|
||||
quit(1)
|
||||
if not _req("选择态应三卡呈现且确认/继续隐藏(btns=%d)" % btns,
|
||||
ov.visible and btns == 3 and not ov._confirm_btn.visible and not ov._continue_btn.visible):
|
||||
return
|
||||
# ② 点选→高亮→确认外发(真实按钮路径);确认前改选以最后点选为准
|
||||
ov.press_card(0)
|
||||
ov.press_card(1)
|
||||
ov.press_confirm()
|
||||
if ov._confirm_btn.visible != true or picked.size() != 1 or String(picked[0]) != "gp":
|
||||
print("[reward_verify] FAIL — 点选确认未外发 pick_confirmed(gp)(%s)" % str(picked))
|
||||
quit(1)
|
||||
if not _req("点选确认未外发 pick_confirmed(gp)(%s)" % str(picked),
|
||||
ov._confirm_btn.visible and picked.size() == 1 and String(picked[0]) == "gp"):
|
||||
return
|
||||
# ③ 替换子面板:丢旧收新与放弃新件两路外发(通关渠道=携带序号 / 节点渠道=库存句柄——tag 直通)
|
||||
ov.open_replace([{"index": 0, "item_id": "probe_dust", "display_name": "显影尘"}], "爆破工兵包", 1, 1)
|
||||
@@ -43,33 +52,26 @@ func _run() -> void:
|
||||
for c in ov._replace_box.get_children():
|
||||
if c is Button:
|
||||
replace_btns += 1
|
||||
if replace_btns != 2 or ov._choice_box.visible:
|
||||
print("[reward_verify] FAIL — 替换面板应 2 钮(N+1 选 N+放弃)且三卡隐藏(btns=%d)" % replace_btns)
|
||||
quit(1)
|
||||
if not _req("替换面板应 2 钮(N+1 选 N+放弃)且三卡隐藏(btns=%d)" % replace_btns,
|
||||
replace_btns == 2 and not ov._choice_box.visible):
|
||||
return
|
||||
var replaced_first := false
|
||||
for c in ov._replace_box.get_children():
|
||||
if c is Button:
|
||||
c.pressed.emit()
|
||||
replaced_first = true
|
||||
break
|
||||
if replaced.size() != 1 or int(replaced[0]) != 0:
|
||||
print("[reward_verify] FAIL — 丢旧收新未外发 replace_chosen(0)(%s)" % str(replaced))
|
||||
quit(1)
|
||||
if not _req("丢旧收新未外发 replace_chosen(0)(%s)" % str(replaced),
|
||||
replaced_first and replaced.size() == 1 and int(replaced[0]) == 0):
|
||||
return
|
||||
# ④ 结果态:继续钮点亮、唯一出口
|
||||
ov.show_result("已选择:攻击力提升 · 攻击 +1")
|
||||
if ov._continue_btn.visible != true or ov._replace_box.visible:
|
||||
print("[reward_verify] FAIL — 结果态应点亮继续且收替换面板")
|
||||
quit(1)
|
||||
if not _req("结果态应点亮继续且收替换面板", ov._continue_btn.visible and not ov._replace_box.visible):
|
||||
return
|
||||
ov.press_continue()
|
||||
if continued.size() != 1:
|
||||
print("[reward_verify] FAIL — 继续未外发 continue_pressed")
|
||||
quit(1)
|
||||
if not _req("继续未外发 continue_pressed", continued.size() == 1):
|
||||
return
|
||||
ov.close_pick()
|
||||
if ov.visible:
|
||||
print("[reward_verify] FAIL — close_pick 应收屏")
|
||||
quit(1)
|
||||
if not _req("close_pick 应收屏", not ov.visible):
|
||||
return
|
||||
print("[reward_verify] PASS — 三卡两段式/确认外发/替换两路/结果态/收屏 全过")
|
||||
quit(0)
|
||||
quit(Kit.finish("reward_verify", "PASS — 三卡两段式/确认外发/替换两路/结果态/收屏 全过"))
|
||||
@@ -9,9 +9,7 @@ extends SceneTree
|
||||
|
||||
const ConfigDBScript := preload("res://core/config_db.gd")
|
||||
const BoardViewScript := preload("res://presentation/views/board_view.gd")
|
||||
|
||||
var _failures := 0
|
||||
var _checks := 0
|
||||
const Kit := preload("res://tests/unit/test_kit.gd") # T6 收口:_check/汇总退出样板单点
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
@@ -23,7 +21,7 @@ func _init() -> void:
|
||||
return
|
||||
var db = ConfigDBScript.new()
|
||||
var load_ok: bool = db.load_tables("res://data/tables", true)
|
||||
_check("前置:表加载零 error", load_ok and db.diagnostics_count("error") == 0)
|
||||
Kit.check("前置:表加载零 error", load_ok and db.diagnostics_count("error") == 0)
|
||||
if not load_ok:
|
||||
quit(1)
|
||||
return
|
||||
@@ -33,25 +31,11 @@ func _init() -> void:
|
||||
var bt := String(row.get("board_type", ""))
|
||||
if bt != "" and not types.has(bt):
|
||||
types.append(bt)
|
||||
_check("前置:board_gen 词表非空(%d 型)" % types.size(), types.size() > 0)
|
||||
Kit.check("前置:board_gen 词表非空(%d 型)" % types.size(), types.size() > 0)
|
||||
_test_set_completeness(board_dir, types)
|
||||
_test_hue_domains(board_dir)
|
||||
_test_digit_ink_modes(board_dir, types)
|
||||
if _failures > 0:
|
||||
push_error("[terrain_tile_sets_test] %d/%d 项失败" % [_failures, _checks])
|
||||
quit(1)
|
||||
return
|
||||
print("[terrain_tile_sets_test] all green (%d checks)" % _checks)
|
||||
quit(0)
|
||||
|
||||
|
||||
func _check(desc: String, ok: bool) -> void:
|
||||
_checks += 1
|
||||
if ok:
|
||||
print(" [PASS] " + desc)
|
||||
else:
|
||||
_failures += 1
|
||||
push_error(" [FAIL] " + desc)
|
||||
quit(Kit.finish("terrain_tile_sets_test"))
|
||||
|
||||
|
||||
# 套完整性:词表每个 board_type 的 16 墙 + 5 地面变体全部在盘(缺位 → BoardView 整批回落 dungeon 的
|
||||
@@ -69,7 +53,7 @@ func _test_set_completeness(board_dir: String, types: Array) -> void:
|
||||
var pf := board_dir.path_join("terrain_floor_%s_%d.png" % [String(bt), i])
|
||||
if not FileAccess.file_exists(pf):
|
||||
missing.append("floor_%d" % i)
|
||||
_check("图型 %s 瓦片套完整(16 墙 + %d 地面,缺=%s)" % [String(bt), variants, str(missing)], missing.is_empty())
|
||||
Kit.check("图型 %s 瓦片套完整(16 墙 + %d 地面,缺=%s)" % [String(bt), variants, str(missing)], missing.is_empty())
|
||||
|
||||
|
||||
# 色相域抽检:wall_center 上半幅(墙顶面)与 floor_1 全幅的均值色相/明度
|
||||
@@ -85,17 +69,17 @@ func _test_hue_domains(board_dir: String) -> void:
|
||||
var wall := _load_stats(board_dir, "terrain_wall_center_%s.png" % String(bt), true)
|
||||
var floor_img := _load_stats(board_dir, "terrain_floor_%s_1.png" % String(bt), false)
|
||||
if wall.is_empty() or floor_img.is_empty():
|
||||
_check("图型 %s 抽检样本可读" % String(bt), false)
|
||||
Kit.check("图型 %s 抽检样本可读" % String(bt), false)
|
||||
continue
|
||||
wall_v[bt] = float(wall["v"])
|
||||
floor_v[bt] = float(floor_img["v"])
|
||||
var band: Array = bands[bt]
|
||||
_check("图型 %s 墙顶均值色相在带 %s(实测 %.3f)" % [String(bt), str(band), float(wall["h"])],
|
||||
Kit.check("图型 %s 墙顶均值色相在带 %s(实测 %.3f)" % [String(bt), str(band), float(wall["h"])],
|
||||
float(wall["h"]) >= float(band[0]) and float(wall["h"]) <= float(band[1]))
|
||||
_check("图型 %s 图底分离(墙顶明度 %.3f > 地面 %.3f)" % [String(bt), wall_v[bt], floor_v[bt]],
|
||||
Kit.check("图型 %s 图底分离(墙顶明度 %.3f > 地面 %.3f)" % [String(bt), wall_v[bt], floor_v[bt]],
|
||||
wall_v[bt] > floor_v[bt])
|
||||
if wall_v.has("underdark") and wall_v.has("deep_underdark"):
|
||||
_check("deep_underdark 墙顶更暗于 underdark(更深蓝黑方向)", float(wall_v["deep_underdark"]) < float(wall_v["underdark"]))
|
||||
Kit.check("deep_underdark 墙顶更暗于 underdark(更深蓝黑方向)", float(wall_v["deep_underdark"]) < float(wall_v["underdark"]))
|
||||
|
||||
|
||||
# 均值读数:hue 取饱和度 > 0.05 的像素(墨线/灰黑不影响色相均值);v 取全幅均值
|
||||
@@ -150,26 +134,26 @@ func _test_digit_ink_modes(board_dir: String, types: Array) -> void:
|
||||
var tex: Texture2D = ImageTexture.create_from_image(Image.load_from_file(p))
|
||||
BoardViewScript._normalize_floor_luma(tex, 0.10, lumas)
|
||||
if not readable:
|
||||
_check("图型 %s 数字墨族断言样本可读" % String(bt), false)
|
||||
Kit.check("图型 %s 数字墨族断言样本可读" % String(bt), false)
|
||||
continue
|
||||
var mode: Dictionary = BoardViewScript._digit_ink_mode(lumas)
|
||||
var plate: bool = bool(mode["plate"])
|
||||
var dark: bool = bool(mode["dark"])
|
||||
var mode_name := "色牌垫底" if plate else ("深墨族直画" if dark else "亮墨族直画")
|
||||
if plate:
|
||||
_check("图型 %s 双峰纹理落牌(亮族 p90 %.2f/均值 %.2f,深族 p90 %.2f/均值 %.2f 均未过门)" % [
|
||||
Kit.check("图型 %s 双峰纹理落牌(亮族 p90 %.2f/均值 %.2f,深族 p90 %.2f/均值 %.2f 均未过门)" % [
|
||||
String(bt), float(mode["cr_p90_bright"]), float(mode["cr_mean_bright"]),
|
||||
float(mode["cr_p90_dark"]), float(mode["cr_mean_dark"])],
|
||||
float(mode["cr_mean_dark"]) < float(BoardViewScript.DIGIT_INK_CR_MEAN_DARK))
|
||||
else:
|
||||
var cr_p90 := float(mode["cr_p90_bright"]) if not dark else float(mode["cr_p90_dark"])
|
||||
var cr_mean := float(mode["cr_mean_bright"]) if not dark else float(mode["cr_mean_dark"])
|
||||
_check("图型 %s 所选%s 对 p90 ≥%.1f(实测 %.2f)" % [String(bt), mode_name,
|
||||
Kit.check("图型 %s 所选%s 对 p90 ≥%.1f(实测 %.2f)" % [String(bt), mode_name,
|
||||
float(BoardViewScript.DIGIT_INK_CR_P90), cr_p90],
|
||||
cr_p90 >= float(BoardViewScript.DIGIT_INK_CR_P90))
|
||||
var mean_gate := float(BoardViewScript.DIGIT_INK_CR_MEAN_BRIGHT) if not dark else float(BoardViewScript.DIGIT_INK_CR_MEAN_DARK)
|
||||
_check("图型 %s 所选%s 对均值 ≥%.1f(实测 %.2f)" % [String(bt), mode_name, mean_gate, cr_mean],
|
||||
Kit.check("图型 %s 所选%s 对均值 ≥%.1f(实测 %.2f)" % [String(bt), mode_name, mean_gate, cr_mean],
|
||||
cr_mean >= mean_gate)
|
||||
if expect_plate.has(bt):
|
||||
_check("图型 %s 呈现模式期望=%s(实测=%s)" % [String(bt),
|
||||
Kit.check("图型 %s 呈现模式期望=%s(实测=%s)" % [String(bt),
|
||||
"色牌" if bool(expect_plate[bt]) else "直画", mode_name], plate == bool(expect_plate[bt]))
|
||||
@@ -7,38 +7,22 @@
|
||||
extends SceneTree
|
||||
|
||||
const UiKitScript := preload("res://presentation/views/ui_kit.gd")
|
||||
|
||||
var _failures := 0
|
||||
var _checks := 0
|
||||
const Kit := preload("res://tests/unit/test_kit.gd") # T6 收口:_check/汇总退出样板单点
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
var path: String = UiKitScript.FONT_UI
|
||||
var f: Font = UiKitScript.load_font(path)
|
||||
_check("随包字体守卫装载非空(res://assets/fonts/ui_font_fusion_pixel_zh_hans.ttf)", f != null)
|
||||
Kit.check("随包字体守卫装载非空(res://assets/fonts/ui_font_fusion_pixel_zh_hans.ttf)", f != null)
|
||||
if f != null:
|
||||
_check("中文字形在集(攻/雷/旗/封)", f.has_char("攻".unicode_at(0)) and f.has_char("雷".unicode_at(0)) \
|
||||
Kit.check("中文字形在集(攻/雷/旗/封)", f.has_char("攻".unicode_at(0)) and f.has_char("雷".unicode_at(0)) \
|
||||
and f.has_char("旗".unicode_at(0)) and f.has_char("封".unicode_at(0)))
|
||||
_check("ASCII/数字字形在集(A/0/%)", f.has_char("A".unicode_at(0)) and f.has_char("0".unicode_at(0)) and f.has_char("%".unicode_at(0)))
|
||||
Kit.check("ASCII/数字字形在集(A/0/%)", f.has_char("A".unicode_at(0)) and f.has_char("0".unicode_at(0)) and f.has_char("%".unicode_at(0)))
|
||||
var sz := 18
|
||||
var w_cn: float = f.get_string_size("战斗胜利", HORIZONTAL_ALIGNMENT_LEFT, -1, sz).x
|
||||
var w_label: float = f.get_string_size("攻击 2", HORIZONTAL_ALIGNMENT_LEFT, -1, sz).x
|
||||
_check("中文采样文本宽度 > 0(C6 headless 判据)", w_cn > 0.0 and w_label > 0.0)
|
||||
Kit.check("中文采样文本宽度 > 0(C6 headless 判据)", w_cn > 0.0 and w_label > 0.0)
|
||||
# 注:不设「私用区 has_char=false」负探针——fusion pixel 实测覆盖 PUA 码位,负向覆盖非契约面(宁缺勿错)
|
||||
else:
|
||||
_check("字体缺失时后续断言跳过(缺位=回归,此行仅占位计数)", false)
|
||||
if _failures > 0:
|
||||
push_error("[ui_font_asset_test] %d/%d 项失败" % [_failures, _checks])
|
||||
quit(1)
|
||||
return
|
||||
print("[ui_font_asset_test] all green (%d checks)" % _checks)
|
||||
quit(0)
|
||||
|
||||
|
||||
func _check(desc: String, ok: bool) -> void:
|
||||
_checks += 1
|
||||
if ok:
|
||||
print(" [PASS] " + desc)
|
||||
else:
|
||||
_failures += 1
|
||||
push_error(" [FAIL] " + desc)
|
||||
Kit.check("字体缺失时后续断言跳过(缺位=回归,此行仅占位计数)", false)
|
||||
quit(Kit.finish("ui_font_asset_test"))
|
||||
@@ -0,0 +1,72 @@
|
||||
# tests/unit/test_kit.gd — 单元测试协议样板单点(T6 样板收口批 2026-10-02)
|
||||
# 收口范围:11 份套件的 _check/_problems 样板(计数器 + PASS/FAIL 行 + 汇总退出)+
|
||||
# reward_dangling_aux/aux_target_violation 两致命类用例各自复制的 BAD 表目录夹具。
|
||||
# 用法(--script 模式无 autoload/全局标识符——禁 class_name 裸引用,使用方 preload):
|
||||
# const Kit := preload("res://tests/unit/test_kit.gd")
|
||||
# Kit.check("断言描述", cond) # 计数 + 即时 PASS/FAIL 行
|
||||
# Kit.check_quiet(cond, "描述") # 计数 + 失败收集(finish 时统一汇总——原 _problems 风格,原参序)
|
||||
# quit(Kit.finish("suite_name")) # 汇总输出 + 返回退出码(0 全绿 / 1 有败)
|
||||
# 计数器为 static(套件 = 独立 Godot 子进程恰一份生命周期,见 run_all.gd 执行形态);
|
||||
# 单进程跑多套件须先 Kit.reset()。
|
||||
extends RefCounted
|
||||
|
||||
static var checks: int = 0
|
||||
static var failures: int = 0
|
||||
static var problems: Array = []
|
||||
|
||||
|
||||
# 主通道:计数 + 即时 PASS/FAIL 行(config_db/rng_service/damage_math/system_contracts/
|
||||
# damage_controller/ui_font_asset/terrain_tile_sets 等套件原 _check 同款输出,描述在前)
|
||||
static func check(desc: String, ok: bool) -> void:
|
||||
checks += 1
|
||||
if ok:
|
||||
print(" [PASS] " + desc)
|
||||
else:
|
||||
failures += 1
|
||||
push_error(" [FAIL] " + desc)
|
||||
|
||||
|
||||
# 静默收集通道:失败只记入 problems(即时零刷屏),finish 统一汇总。
|
||||
# 参序沿原函数式套件 aux_items/breakpoint_resume 的 check(cond, msg)——条件在前,与主通道相反。
|
||||
static func check_quiet(ok: bool, desc: String) -> void:
|
||||
checks += 1
|
||||
if not ok:
|
||||
failures += 1
|
||||
problems.append(desc)
|
||||
|
||||
|
||||
# 汇总退出:返回退出码(调用方 `quit(Kit.finish(...))`)。pass_line = 成功尾行覆盖
|
||||
#(保留原套件 PASS 摘要文案;缺省 = "all green (N checks)" 计数口径)。
|
||||
# 失败路径:push_error 计数汇总 + problems 逐条 FAIL 行(print + push_error 双通道——原两风格并档)。
|
||||
static func finish(suite: String, pass_line: String = "") -> int:
|
||||
if failures > 0:
|
||||
push_error("[%s] %d/%d 项失败" % [suite, failures, checks])
|
||||
for p in problems:
|
||||
print("[%s] FAIL — %s" % [suite, p])
|
||||
push_error("[%s] FAIL — %s" % [suite, p])
|
||||
return 1
|
||||
print("[%s] %s" % [suite, pass_line if pass_line != "" else "all green (%d checks)" % checks])
|
||||
return 0
|
||||
|
||||
|
||||
# BAD 表夹具共享(原 reward_dangling_aux/aux_target_violation 各自复制的 _copy_tables):
|
||||
# 整目录拷贝 src_dir → bad_dir(目录已建)。BAD_DIR 常量仍由各用例自持(名字互异——独立进程并行不互踩)。
|
||||
static func copy_tables(src_dir: String, bad_dir: String) -> bool:
|
||||
DirAccess.make_dir_recursive_absolute(bad_dir)
|
||||
var dir := DirAccess.open(src_dir)
|
||||
if dir == null:
|
||||
return false
|
||||
dir.list_dir_begin()
|
||||
var f := dir.get_next()
|
||||
while f != "":
|
||||
if not dir.current_is_dir():
|
||||
if dir.copy(src_dir + "/" + f, bad_dir + "/" + f) != OK:
|
||||
return false
|
||||
f = dir.get_next()
|
||||
return true
|
||||
|
||||
|
||||
static func reset() -> void:
|
||||
checks = 0
|
||||
failures = 0
|
||||
problems = []
|
||||
@@ -0,0 +1 @@
|
||||
uid://dtestkit123456
|
||||
Reference in new issue
Block a user