iter10 T3-T9: 奖励重构实装——adventure表扩列(回血20/池六种/权重/上限/同调位过渡键)+boss_keystone重锚3-2+rng扩签reward_pick+E-S加权洗牌+三选一停点(零自动推进/可中断)+加算区四轴AP(BattleOrchestrator排队兑现)+钥石逐杀掉落(收束无二次派生)+同调位约束(停点替换两路/战斗沉没/节点拒绝)+BonusPickOverlay+Narrator文案族+save v3迁移+冒烟619/0(+23断言)+断点P5往返单测+run_all 15套全绿
This commit is contained in:
1 parent
49b263fbc8
commit
71502ceecd
18 files changed
+992
-169
No files matched your search
@@ -15,9 +15,9 @@
|
||||
# 驱动循环按 route 策略显式 pick_next_node;route = "min"/"max"(候选节点编号最小/最大——
|
||||
# 确定性路径轴,零随机流自开;默认 "min" 既有调用方签名兼容)。
|
||||
# 2026-09-21 冒险结构实现批:run_adventure 三关连探驱动(AdventureRunner——主种子恰一次、
|
||||
# 大奖励立即确认 continue_to_next_level、adventure_finished 置 result/summary;外层每关重取
|
||||
# runner 实例经 _drive_level 复用单关驱动)。run_level 驱动段抽成 _drive_level——签名与
|
||||
# 返回结构零变化,行为逐位不变。
|
||||
# 通关兑付停点确认、adventure_finished 置 result/summary;外层每关重取 runner 实例经 _drive_level
|
||||
# 复用单关驱动)。run_level 驱动段抽成 _drive_level——签名与返回结构零变化,行为逐位不变。
|
||||
# 2026-10-01 迭代 10 奖励重构批:大奖励立即确认改三选一停点驱动(候选取首件/替换放弃新件)。
|
||||
# 运行前提:db 已 load_tables;run_level/run_adventure 均须全新 RngService(同种子 ⇒ 全程逐步可回放)。
|
||||
# 实现注:信号处理体全部为静态方法 + bind 传状态——GDScript 多行 lambda 内嵌 match/控制流的解析不可靠。
|
||||
extends RefCounted
|
||||
@@ -124,7 +124,11 @@ static func run_adventure(db, seed_val: int, atk_pct: int, hp_pct: int, collect_
|
||||
adr.step_row.connect(_on_step_row.bind(adr, st, collect_rows))
|
||||
adr.node_entered.connect(_on_adventure_node_entered.bind(adr, st, logger))
|
||||
adr.wallet_changed.connect(_on_adventure_wallet.bind(st, logger))
|
||||
adr.big_reward_paid.connect(_on_adventure_reward.bind(st, adr))
|
||||
# 三选一停点驱动(迭代 10 奖励重构批——big_reward_paid 退役:确定性策略候选取首件、
|
||||
# 道具件满位替换放弃新件、兑付后推进下一关——零随机流自开,同种子逐位可复放)
|
||||
adr.bonus_choice_presented.connect(_on_adventure_bonus.bind(st, adr))
|
||||
adr.bonus_resolved.connect(_on_adventure_bonus_resolved.bind(st, adr))
|
||||
adr.aux_replace_presented.connect(_on_adventure_aux_replace.bind(st, adr))
|
||||
adr.adventure_finished.connect(_on_adventure_finished.bind(st, logger))
|
||||
adr.set_hero_override(atk_pct, hp_pct)
|
||||
if not adr.start_adventure(seed_val, {"hero_override": hero_override, "first_script": false}): # 测量夹具恒抽签(v0.4——首次脚本分支不进任何读数)
|
||||
@@ -132,7 +136,7 @@ static func run_adventure(db, seed_val: int, atk_pct: int, hp_pct: int, collect_
|
||||
if own_session:
|
||||
logger.close_session()
|
||||
return st
|
||||
var guard_drives := 10 # 每次外层迭代 = 驱动一关(关卡更替由 big_reward 确认与 runner 重取承载)
|
||||
var guard_drives := 10 # 每次外层迭代 = 驱动一关(关卡更替由三选一兑付确认与 runner 重取承载)
|
||||
while st.result == "" and guard_drives > 0:
|
||||
guard_drives -= 1
|
||||
_drive_level(adr.runner(), st, logger, policy, route)
|
||||
@@ -183,18 +187,34 @@ static func _on_adventure_node_entered(node: Dictionary, info: Dictionary, adr,
|
||||
# 冒险钱包事件采集(wallet 行族——GP 跨关延续与冒险收束 clear 的验收取数面;同日志合规转发)
|
||||
static func _on_adventure_wallet(event: String, gp_val: int, extra: Dictionary, st: Dictionary, logger) -> void:
|
||||
st.wallet_events.append({"event": String(event), "gp": int(gp_val),
|
||||
"amount": int(extra.get("amount", 0)), "product": String(extra.get("product", ""))})
|
||||
"amount": int(extra.get("amount", 0)), "product": String(extra.get("product", "")),
|
||||
"source": String(extra.get("source", ""))})
|
||||
if logger != null:
|
||||
logger.log_wallet(event, gp_val, extra)
|
||||
|
||||
|
||||
# 大奖励立即确认(非末关通关 → continue_to_next_level 进下一关——自动路径无大奖励屏停点)
|
||||
static func _on_adventure_reward(reward: Dictionary, st: Dictionary, adr) -> void:
|
||||
st.reward_events.append(reward.duplicate(true))
|
||||
# 三选一停点驱动(迭代 10——确定性策略:候选取首件;奖励事件随 reward_events 留档)
|
||||
static func _on_adventure_bonus(payload: Dictionary, st: Dictionary, adr) -> void:
|
||||
st.reward_events.append({"bonus_pick": payload.duplicate(true)})
|
||||
var candidates: Array = payload.get("candidates", []) as Array
|
||||
if candidates.is_empty() or not adr.pick_bonus(String(candidates[0])):
|
||||
st.result = "advance_failed"
|
||||
|
||||
|
||||
# 三选一兑付完成 → 推进下一关(自动路径无停点等待——零自动推进契约由显式确认满足)
|
||||
static func _on_adventure_bonus_resolved(result: Dictionary, st: Dictionary, adr) -> void:
|
||||
st.reward_events.append({"bonus_resolved": result.duplicate(true)})
|
||||
if not adr.continue_to_next_level():
|
||||
st.result = "advance_failed"
|
||||
|
||||
|
||||
# 同调位满位替换子交互(确定性策略:放弃新件不获取——-1)
|
||||
static func _on_adventure_aux_replace(items: Array, incoming: Dictionary, st: Dictionary, adr) -> void:
|
||||
st.reward_events.append({"aux_replace": incoming.duplicate(true)})
|
||||
if not adr.resolve_aux_replace(-1):
|
||||
st.result = "advance_failed"
|
||||
|
||||
|
||||
# 冒险终局(completed/failed/abandoned——result 载 summary.result,外层驱动循环随之收敛)
|
||||
static func _on_adventure_finished(summary: Dictionary, st: Dictionary, logger) -> void:
|
||||
st.summary = summary.duplicate(true)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://dv81k73ruw7xe
|
||||
+181
-67
@@ -189,17 +189,27 @@ func _smoke_rest_stub(pct: int, hp: int, max_hp: int) -> int:
|
||||
|
||||
# ---- 冒险结构实现批冒烟(2026-09-21——AdventureTable 建表 + AdventureRunner + 冒险验收断言)----
|
||||
|
||||
# ① 冒险表(单行全局 Rule 4 豁免——建表与接线闸)
|
||||
# ① 冒险表(单行全局 Rule 4 豁免——建表与接线闸;迭代 10 扩列域检)
|
||||
func _smoke_adventure_table() -> void:
|
||||
print("--- 冒险表(AdventureTable 建表——大奖励/钥石行值单点)---")
|
||||
print("--- 冒险表(AdventureTable——三层奖励框架行值单点:回血/件值/权重/上限/同调位)---")
|
||||
check(_db.adventure != null, "db.adventure 已加载(adventure.tres 入 manifest)")
|
||||
if _db.adventure == null:
|
||||
return
|
||||
check(_db.adventure.validate().is_empty(), "AdventureTable.validate() 零违例(两字段域检在案)")
|
||||
var hp_bonus: int = int(_db.adventure.level_clear_hp_bonus)
|
||||
var ks: int = int(_db.adventure.keystone_per_level)
|
||||
check(hp_bonus >= 0 and hp_bonus <= 999, "level_clear_hp_bonus ∈ [0,999](%d)" % hp_bonus)
|
||||
check(ks >= 0 and ks <= 999, "keystone_per_level ∈ [0,999](%d)" % ks)
|
||||
check(_db.adventure.validate().is_empty(), "AdventureTable.validate() 零违例(全键域检在案)")
|
||||
check(int(_db.adventure.clear_heal_pct) >= 0 and int(_db.adventure.clear_heal_pct) <= 100,
|
||||
"clear_heal_pct ∈ [0,100](%d——通关部分回血比例)" % int(_db.adventure.clear_heal_pct))
|
||||
for id in _db.adventure.BONUS_IDS:
|
||||
var v := int(_db.adventure.bonus_value(id))
|
||||
var w := int(_db.adventure.bonus_weight(id))
|
||||
check(v >= 0 and v <= 999 and w >= 1 and w <= 99, "三选一件 %s:值 ∈[0,999](%d)· 权重 ∈[1,99](%d)" % [id, v, w])
|
||||
check(int(_db.adventure.bonus_cap_ap) == 2 and int(_db.adventure.bonus_cap_attack) == int(_db.adventure.bonus_cap_max_hp)
|
||||
and int(_db.adventure.bonus_cap_max_hp) == int(_db.adventure.bonus_cap_armor),
|
||||
"次数上限口径:AP 件=%d(全冒险最紧)· 属性件=%d(三件同)" % [int(_db.adventure.bonus_cap_ap), int(_db.adventure.bonus_cap_attack)])
|
||||
check(int(_db.adventure.bonus_cap("gp")) == _db.adventure.BONUS_CAP_UNLIMITED
|
||||
and int(_db.adventure.bonus_cap("aux_item")) == _db.adventure.BONUS_CAP_UNLIMITED,
|
||||
"GP/道具件无次数上限(牌库恒非空的结构保证)")
|
||||
check(int(_db.adventure.transitional_sync_slots) >= 0 and int(_db.adventure.transitional_sync_slots) <= 9,
|
||||
"transitional_sync_slots ∈ [0,9](%d——同调位过渡读数源)" % int(_db.adventure.transitional_sync_slots))
|
||||
|
||||
|
||||
# ② 冒险结构 API 闸(状态机受理面——重复开始拒绝 / 入口停点 / 放弃两态)
|
||||
@@ -279,43 +289,86 @@ func _smoke_adventure() -> void:
|
||||
if not bg_keys.has(key) or not en_keys.has(key):
|
||||
miss += 1
|
||||
check(miss == 0, "行集内每个战斗/Boss 节点在 board_gen + enemy 有精确行(缺失 %d)" % miss)
|
||||
# ② 满配通关 + ④ 大奖励兑付(恰 2 次 · 读数=表值 · 回满=新上限 · 覆盖值通道传播 L2/L3 首战上限)
|
||||
# ② 满配通关 + ④ 三层奖励兑付(三选一恰 2 次 · 回血封顶 · 钥石逐杀 · 候选零重复)
|
||||
var st: Dictionary = autoplay.run_adventure(_db, 42, 900, 400)
|
||||
var sum: Dictionary = st["summary"]
|
||||
check(String(st["result"]) == "completed" and bool(sum.get("cleared", false)),
|
||||
"满配通关(42 / atk+900 / hp+400——cleared=true;v2 怪阶梯后旧 200/100 档不再满配)")
|
||||
check(int(sum.get("levels_cleared", -1)) == 3, "通关几关 = 3(levels_cleared)")
|
||||
check(int(sum.get("keystone_total", -1)) == 3 * int(_db.adventure.keystone_per_level),
|
||||
"钥石合计 = 3 × keystone_per_level(%d)" % int(sum.get("keystone_total", -1)))
|
||||
var ks_exp := 0
|
||||
for li in range(1, int(_db.level.rows.size()) + 1):
|
||||
ks_exp += int(_db.meta_boss_keystone_row(li).get("first_kill", 0))
|
||||
check(int(sum.get("keystone_total", -1)) == ks_exp,
|
||||
"钥石合计 = ΣBoss 首杀行值(%d——逐杀掉落新口径;断点未注入恒首杀)" % int(sum.get("keystone_total", -1)))
|
||||
var drops: Array = sum.get("keystone_drops", []) as Array
|
||||
check(drops.size() == 3, "逐杀掉落明细恰 3 笔(L1/L2/L3——additive 摘要键 keystone_drops)")
|
||||
check(int(sum.get("master_seed", -1)) == 42, "摘要携带主种子(42)")
|
||||
var rewards: Array = st["reward_events"]
|
||||
check(rewards.size() == 2, "大奖励恰兑付 2 次(L1/L2 后——末关不发上限提升)")
|
||||
var hp_bonus: int = int(_db.adventure.level_clear_hp_bonus)
|
||||
var reward_ok: bool = rewards.size() == 2
|
||||
if reward_ok:
|
||||
for i in rewards.size():
|
||||
var r: Dictionary = rewards[i]
|
||||
if int(r.get("hp_bonus", -1)) != hp_bonus or int(r.get("from_level", -1)) != i + 1 \
|
||||
or int(r.get("new_max_hp", -1)) != int(_db.hero.max_hp) + hp_bonus * (i + 1) \
|
||||
or int(r.get("keystone_gained", -1)) != int(_db.adventure.keystone_per_level):
|
||||
reward_ok = false
|
||||
check(reward_ok, "大奖励读数:hp_bonus=表值 · 新上限累加 · 回满=新上限 · 钥石逐关注账")
|
||||
var picks: Array = []
|
||||
var resolves: Array = []
|
||||
for ev in (st["reward_events"] as Array):
|
||||
if (ev as Dictionary).has("bonus_pick"):
|
||||
picks.append((ev as Dictionary)["bonus_pick"])
|
||||
elif (ev as Dictionary).has("bonus_resolved"):
|
||||
resolves.append((ev as Dictionary)["bonus_resolved"])
|
||||
check(picks.size() == 2 and resolves.size() == 2,
|
||||
"三选一恰呈现 2 次、恰兑付 2 次(L1/L2 后——末关不发,奖励只朝下一战生效)")
|
||||
var heal_ok := picks.size() == 2
|
||||
var cand_ok := picks.size() == 2
|
||||
var ks_gain_ok := picks.size() == 2
|
||||
var resolve_ok := resolves.size() == 2
|
||||
var max_flat := int(_db.adventure.bonus_max_hp_flat)
|
||||
var heal_pct := int(_db.adventure.clear_heal_pct)
|
||||
var hp_acc := 0 # max_hp 件累加(回血封顶断言的生效上限推导)
|
||||
var ks_prefix := 0
|
||||
for i in picks.size():
|
||||
var p: Dictionary = picks[i]
|
||||
var cands: Array = p.get("candidates", []) as Array
|
||||
if cands.size() < 2 or cands.size() > 3:
|
||||
cand_ok = false
|
||||
var seen := {}
|
||||
for c in cands:
|
||||
if seen.has(String(c)):
|
||||
cand_ok = false
|
||||
seen[String(c)] = true
|
||||
var heal: int = int(p.get("heal_gained", -1))
|
||||
var hp_before: int = int(p.get("hp_before", -1))
|
||||
var hp_after: int = int(p.get("hp_after", -1))
|
||||
var max_eff := int(_db.hero.max_hp) + hp_acc # 回血口径 = 表值+加算区(无 TunePanel 百分比)
|
||||
if heal != mini(int(floor(float(max_eff) * float(heal_pct) / 100.0)), max_eff - hp_before) \
|
||||
or hp_after != hp_before + heal or hp_after > max_eff:
|
||||
heal_ok = false
|
||||
ks_prefix += int(_db.meta_boss_keystone_row(int(p.get("level_index", -1))).get("first_kill", 0))
|
||||
if int(p.get("keystone_gained", -1)) != int(_db.meta_boss_keystone_row(int(p.get("level_index", -1))).get("first_kill", 0)) \
|
||||
or int(p.get("keystone_total", -1)) != ks_prefix:
|
||||
ks_gain_ok = false
|
||||
if resolve_ok:
|
||||
var r: Dictionary = resolves[i]
|
||||
if not (p.get("candidates", []) as Array).has(String(r.get("picked", ""))) \
|
||||
or int(r.get("value", -1)) != int(_db.adventure.bonus_value(String(r.get("picked", "")))):
|
||||
resolve_ok = false
|
||||
if String(r.get("picked", "")) == "max_hp":
|
||||
hp_acc += max_flat
|
||||
check(cand_ok, "三选一候选 2–3 件且零重复(加权洗牌不放回)")
|
||||
check(heal_ok, "通关部分回血封顶(floor(上限×比例) 封顶缺口——血回满退役)")
|
||||
check(ks_gain_ok, "钥石逐杀入账(首杀档位 + 累计逐位)")
|
||||
check(resolve_ok, "点选恰兑付选中一件(值=表值;未选项零生效)")
|
||||
var seq: Array = st["encounter_max_hp_seq"]
|
||||
var k := 5.0 # hp+400 ⇒ TunePanel 百分比后乘 ×5(加算先加、百分比后乘——生效次序口径)
|
||||
var exp_base := maxi(1, int(round(float(int(_db.hero.max_hp)) * k)))
|
||||
var exp_l2 := maxi(1, int(round(float(int(_db.hero.max_hp) + hp_bonus) * k)))
|
||||
var exp_l3 := maxi(1, int(round(float(int(_db.hero.max_hp) + 2 * hp_bonus) * k)))
|
||||
var idx_l2 := seq.find(exp_l2)
|
||||
var idx_l3 := seq.find(exp_l3)
|
||||
check(seq.size() > 0 and int(seq[0]) == exp_base,
|
||||
"L1 首战上限入参 = 基础表值(%d——加算区冒险开始全 0)" % exp_base)
|
||||
check(idx_l2 > 0 and idx_l3 > idx_l2,
|
||||
"L2/L3 首战上限入参 = 基础+加算(%d→%d——大奖励经覆盖值通道传播)" % [exp_l2, exp_l3])
|
||||
# ③ 种子域可复现(同种子两局:summary + 上限序列 + 行进序列逐位一致)
|
||||
var valid_hp := {}
|
||||
for acc in 3:
|
||||
valid_hp[maxi(1, int(round(float(int(_db.hero.max_hp) + acc * max_flat) * k)))] = true
|
||||
var seq_ok := seq.size() > 0 and int(seq[0]) == exp_base
|
||||
for v in (seq as Array):
|
||||
if not valid_hp.has(int(v)):
|
||||
seq_ok = false
|
||||
check(seq_ok,
|
||||
"各战上限入参 ∈ {基础, 基础+件值, 基础+2×件值}(覆盖值通道传播——首战=基础表值 %d;AP 件不加上限)" % exp_base)
|
||||
# ③ 种子域可复现(同种子两局:summary + 上限序列 + 行进序列 + 三选一停点载荷逐位一致)
|
||||
var st2: Dictionary = autoplay.run_adventure(_db, 42, 900, 400)
|
||||
check(st2["summary"] == sum and st2["encounter_max_hp_seq"] == seq
|
||||
and st2["visited_ids"] == st["visited_ids"],
|
||||
"同种子两局逐位可复现(summary/上限序列/行进序列——主种子单点派生)")
|
||||
and st2["visited_ids"] == st["visited_ids"] and st2["reward_events"] == st["reward_events"],
|
||||
"同种子两局逐位可复现(summary/上限序列/行进序列/三选一载荷——reward_pick 封闭流)")
|
||||
# ⑤ 败北线(零配 L1 败北 + 半配 L2 败北——种子扫描,仿商店房 range(42,92) 先例)
|
||||
var lose := {}
|
||||
for s in range(42, 92):
|
||||
@@ -343,9 +396,9 @@ func _smoke_adventure() -> void:
|
||||
if not half.is_empty():
|
||||
var sh: Dictionary = half["st"]["summary"]
|
||||
check(not bool(sh["cleared"]) and int(sh["levels_cleared"]) == 1 \
|
||||
and int(sh["keystone_total"]) == int(_db.adventure.keystone_per_level) \
|
||||
and int(sh["keystone_total"]) == int(_db.meta_boss_keystone_row(1).get("first_kill", 0)) \
|
||||
and int(sh["stopped_at"]) == 2,
|
||||
"L2 败北摘要:L1 已过(钥石 1×表值)· 止步第 2 层")
|
||||
"L2 败北摘要:L1 已过(钥石=L1 首杀掉落)· 止步第 2 层")
|
||||
var dh: Dictionary = sh.get("defeat", {})
|
||||
check(int(dh.get("level_index", -1)) == 2 and int(dh.get("node_id", -1)) >= 0 \
|
||||
and String(dh.get("node_type", "")) in ["battle", "boss"],
|
||||
@@ -372,15 +425,15 @@ func _smoke_adventure() -> void:
|
||||
for ev in wallet:
|
||||
if String(ev["event"]) == "clear":
|
||||
clears += 1
|
||||
elif String(ev["event"]) == "gain":
|
||||
gains += 1
|
||||
check(gains == int(st["battles"]), "每战完成入账恰一次(gain %d = battles %d)" % [gains, int(st["battles"])])
|
||||
elif String(ev["event"]) == "gain" and String(ev.get("source", "")) != "bonus_pick":
|
||||
gains += 1 # 战斗完成入账计数(三选一 GP 袋件入账另有 source 标记——不计战斗口径)
|
||||
check(gains == int(st["battles"]), "每战完成入账恰一次(gain %d = battles %d——GP 袋件入账单列)" % [gains, int(st["battles"])])
|
||||
check(clears == 1 and wallet.size() > 0 and String(wallet[wallet.size() - 1]["event"]) == "clear",
|
||||
"钱包 clear 恰一次且为末事件(关卡收束不清——GP 冒险收束清零归冒险结构)")
|
||||
var shop_seq: Array = st["shop_gp_seq"]
|
||||
check(shop_seq.size() == 2 and int(shop_seq[0]) > 0,
|
||||
"L2 商店房到达余额 > 0(%d——L1 积累跨关延续;两关商店房必经各 1)" % (shop_seq[0] if shop_seq.size() > 0 else -1))
|
||||
# ⑧ 加算区同账(手工场景:L2 商店购买 → 大奖励直调——同一本账累加 + 回满新上限)
|
||||
# ⑧ 加算区同账(手工场景:L2 商店购买 → 三选一直调——同一本账累加 + 部分回血封顶)
|
||||
var same := {}
|
||||
for s in range(42, 92):
|
||||
var a8 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(s))
|
||||
@@ -406,14 +459,22 @@ func _smoke_adventure() -> void:
|
||||
check(gp0 >= hp_price + l2_price_bonus,
|
||||
"进店余额 ≥ hp_up L2 首价(%d = 基价 %d + 关卡加成 %d——配置推导)" % [hp_price + l2_price_bonus, hp_price, l2_price_bonus])
|
||||
check(r8.buy_product("hp_up"), "购买 hp_up 受理(出账 + 加算区)")
|
||||
check(int(a8.bonus.get("max_hp", -1)) == int(_db.adventure.level_clear_hp_bonus) + 3,
|
||||
"统一冒险加算区含商店值(max_hp = %d[L1 大奖励] + 3[商店]——同一本账前半)" % int(_db.adventure.level_clear_hp_bonus))
|
||||
var acc_shop := int(a8.bonus.get("max_hp", -1)) # L1 三选一后 + 商店 3(首件随种子)
|
||||
check(acc_shop >= 3, "统一冒险加算区含商店值(max_hp=%d ≥ 商店 3——L1 三选一件值另计)" % acc_shop)
|
||||
a8._on_level_finished({"result": "victory", "hp": int(r8.hero_hp), "node_index": r8.node_index,
|
||||
"node_id": r8.current_node_id(), "node_type": "shop"}) # 模拟大奖励(victory 载荷直调——L2 非末关)
|
||||
check(int(a8.bonus.get("max_hp", -1)) == 2 * int(_db.adventure.level_clear_hp_bonus) + 3,
|
||||
"大奖励与商店购买同账累加(max_hp = %d + 3 + %d)" % [int(_db.adventure.level_clear_hp_bonus), int(_db.adventure.level_clear_hp_bonus)])
|
||||
check(int(a8.ledger["hero_hp"]) == int(_db.hero.max_hp) + int(a8.bonus["max_hp"]),
|
||||
"血回满 = 新上限(%d/%d)" % [int(a8.ledger["hero_hp"]), int(_db.hero.max_hp) + int(a8.bonus["max_hp"])])
|
||||
"node_id": r8.current_node_id(), "node_type": "shop"}) # 模拟 L2 通关(victory 载荷直调——非末关)
|
||||
check(not a8._bonus_pending.is_empty(), "L2 通关三选一停点在案(直调兑付前——零自动推进)")
|
||||
check(not a8.continue_to_next_level(), "未兑付推进拒绝(选择停点零自动推进契约)")
|
||||
var l2_cands: Array = a8._bonus_pending["candidates"]
|
||||
check(a8.pick_bonus(String(l2_cands[0])), "三选一点选兑付受理(候选取首件)")
|
||||
var picked2: String = String(a8._bonus_result.get("picked", ""))
|
||||
var expect_acc: int = acc_shop + (int(_db.adventure.bonus_max_hp_flat) if picked2 == "max_hp" else 0)
|
||||
check(int(a8.bonus.get("max_hp", -1)) == expect_acc,
|
||||
"三选一属性件与商店购买同账累加(max_hp=%d = %d + 三选一%s)" % [int(a8.bonus.get("max_hp", -1)), acc_shop, picked2])
|
||||
var max_eff8 := int(_db.hero.max_hp) + int(a8.bonus["max_hp"])
|
||||
check(int(a8.ledger["hero_hp"]) <= max_eff8,
|
||||
"通关部分回血封顶(hp %d ≤ 生效上限 %d——血回满语义退役)" % [int(a8.ledger["hero_hp"]), max_eff8])
|
||||
check(a8.continue_to_next_level(), "兑付后推进受理(下一关开始)")
|
||||
# ⑨ 恢复点数学(接口③封顶公式三读数 + 同式两落点一致)
|
||||
var a9 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(99))
|
||||
check(int(a9.apply_rest(30, 70, 100)) == 30 and int(a9.apply_rest(30, 95, 100)) == 5 \
|
||||
@@ -436,6 +497,42 @@ func _smoke_adventure() -> void:
|
||||
g10 -= 1
|
||||
autoplay._autoplay_battle(a10.runner(), b10, st10, null)
|
||||
check(a10.runner().current_battle() == null and a10.abandon(), "非战斗期(选路态)abandon 受理")
|
||||
# ⑪ 上限剔牌库 + 同调位替换两路 + 满血回 0(迭代 10——white-box:计数/携带面直设)
|
||||
var a11 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(11))
|
||||
check(a11.start_adventure(11, {"first_script": true, "aux_unlocked": {"blast_kit": true}}),
|
||||
"剔牌库/替换场景:冒险启动(seed 11——道具解锁集只含 blast_kit)")
|
||||
a11._bonus_taken["attack"] = int(_db.adventure.bonus_cap_attack) # 直设计数(white-box——计数面本身即断点承载)
|
||||
a11._bonus_taken["ap"] = int(_db.adventure.bonus_cap_ap)
|
||||
var draw: Array = a11._draw_bonus_candidates()
|
||||
check(not (draw as Array).has("attack") and not (draw as Array).has("ap"),
|
||||
"达上限件剔牌库(attack 计满 %d / ap 计满 %d 不出现在候选——%s)" % [int(_db.adventure.bonus_cap_attack), int(_db.adventure.bonus_cap_ap), str(draw)])
|
||||
check((draw as Array).size() >= 2, "剔牌库后候选恒 ≥2(GP/道具件无次数上限——防「三选项全不可选」死角)")
|
||||
# 同调位替换·丢旧收新路(slots 过渡键=1,carry 满 1 件再取道具件 → 满位转替换)
|
||||
var slots := int(_db.adventure.transitional_sync_slots)
|
||||
a11._bonus_pending = {"candidates": ["aux_item", "gp", "attack"], "payload": {}}
|
||||
a11._bonus_aux_carry.assign(["probe_dust"])
|
||||
check(a11.pick_bonus("aux_item") and a11._bonus_pending.has("incoming"),
|
||||
"满位道具件转替换子交互(持有 %d/%d)" % [a11._bonus_aux_carry.size(), slots])
|
||||
check(a11.resolve_aux_replace(0) and (a11._bonus_aux_carry as Array).has("blast_kit") \
|
||||
and (a11._bonus_aux_carry as Array).size() <= slots,
|
||||
"替换受理·丢旧收新(终态 ≤ 同调位 %d——%s)" % [slots, str(a11._bonus_aux_carry)])
|
||||
# 同调位替换·放弃新件路(原持有集不动、新件不获取)
|
||||
a11._bonus_pending = {"candidates": ["aux_item"], "payload": {}}
|
||||
a11._bonus_result = {}
|
||||
a11._bonus_aux_carry.assign(["probe_dust"])
|
||||
check(a11.pick_bonus("aux_item"), "替换场景二:满位再转替换子交互")
|
||||
check(a11.resolve_aux_replace(-1) and a11._bonus_aux_carry.size() == 1 and String(a11._bonus_aux_carry[0]) == "probe_dust",
|
||||
"替换受理·放弃新件(原持有集不动、新件不获取——两路终态恒 ≤ 同调位)")
|
||||
# 满血回 0 合法(通关回血封顶口径)
|
||||
var a12 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(12))
|
||||
a12.start_adventure(12, {"first_script": true})
|
||||
var cap := {"payload": {}}
|
||||
a12.bonus_choice_presented.connect(func(p): cap["payload"] = p)
|
||||
var max_full := int(_db.hero.max_hp) + int(a12._base_override.get("max_hp", 0))
|
||||
a12.ledger["hero_hp"] = max_full
|
||||
a12._on_level_finished({"result": "victory", "hp": max_full, "node_index": 1, "node_id": 1, "node_type": "shop"})
|
||||
check(int(cap["payload"].get("heal_gained", -1)) == 0,
|
||||
"满血通关回血 0 合法(封顶口径——heal_gained=0 非负溢出)")
|
||||
var s10: Dictionary = a10.summary
|
||||
check(String(s10.get("result", "")) == "abandoned" and not bool(s10.get("cleared", true)),
|
||||
"放弃摘要:abandoned · 未打穿(已获大奖励不追回——同失败型结算)")
|
||||
@@ -450,6 +547,10 @@ func _adv_drive_to_shop(a, st: Dictionary, autoplay) -> bool:
|
||||
if String(st["result"]) != "":
|
||||
return false
|
||||
if a._pending_next_level:
|
||||
# 三选一停点确认(迭代 10——候选取首件后推进,与 run_adventure 自动策略同口径)
|
||||
if not a._bonus_pending.is_empty():
|
||||
var bcands: Array = a._bonus_pending["candidates"]
|
||||
a.pick_bonus(String(bcands[0]))
|
||||
a.continue_to_next_level()
|
||||
continue
|
||||
var r = a.runner()
|
||||
@@ -3397,27 +3498,37 @@ func _meta_settle_and_workshop() -> void:
|
||||
var mp = load("res://core/meta_progression.gd").new(_db, sv)
|
||||
var row1: Dictionary = _db.meta_boss_keystone_row(1)
|
||||
var row2: Dictionary = _db.meta_boss_keystone_row(2)
|
||||
# ⑥ 新档首通 L1:first = true、金额 = first_kill;total = big_reward + Σ drops;余额 = 累计入账
|
||||
# ⑥ 逐杀掉落收束入账(迭代 10——掉落已在冒险内逐杀入账〔summary.keystone_total 含全部〕,
|
||||
# keystone_drops 明细随摘要归档;首杀标记仍由 apply 逐通关关次置位)
|
||||
var d1: Dictionary = mp.settle_adventure({"result": "failed", "cleared": false, "levels_cleared": 1,
|
||||
"stopped_at": 2, "keystone_total": 3, "master_seed": 11})
|
||||
"stopped_at": 2, "keystone_total": int(row1.get("first_kill", 0)),
|
||||
"keystone_drops": [{"level": 1, "first": true, "amount": int(row1.get("first_kill", 0))}],
|
||||
"master_seed": 11})
|
||||
var drop1: Dictionary = d1["boss_drops"][0]
|
||||
check(bool(drop1.get("first", false)) and int(drop1.get("amount", 0)) == int(row1.get("first_kill", -1))
|
||||
and int(drop1.get("level", 0)) == 1, "新档首通 L1:boss_drops[0] {first:true, amount=first_kill}(验收5)")
|
||||
check(int(d1.get("total", 0)) == 3 + int(row1.get("first_kill", 0)), "明细 total = big_reward + Σ drops")
|
||||
check(sv.keystone_balance() == 3 + int(row1.get("first_kill", 0)), "余额 = 累计入账(大奖励 + Boss 两档)")
|
||||
# ⑥ 同档再通 → 重复杀档且恒定不递减(第三轮同额)
|
||||
and int(drop1.get("level", 0)) == 1, "新档首通 L1:boss_drops[0] {first:true, amount=first_kill}(验收5——明细随摘要归档)")
|
||||
check(int(d1.get("total", 0)) == int(row1.get("first_kill", 0)), "明细 total = keystone_total(逐杀掉落全源——无二次派生加账)")
|
||||
check(sv.keystone_balance() == int(row1.get("first_kill", 0)), "余额 = 累计入账(逐杀掉落合计——无重复加账)")
|
||||
# ⑥ 再通转复杀档(首杀标记置位后小额恒定不递减——标记面在 apply,金额面在逐杀裁决)
|
||||
var d2: Dictionary = mp.settle_adventure({"result": "failed", "cleared": false, "levels_cleared": 1,
|
||||
"stopped_at": 2, "keystone_total": 3, "master_seed": 12})
|
||||
"stopped_at": 2, "keystone_total": int(row1.get("repeat_kill", 0)),
|
||||
"keystone_drops": [{"level": 1, "first": false, "amount": int(row1.get("repeat_kill", 0))}],
|
||||
"master_seed": 12})
|
||||
var drop2: Dictionary = d2["boss_drops"][0]
|
||||
check(not bool(drop2.get("first", false)) and int(drop2.get("amount", 0)) == int(row1.get("repeat_kill", -1)),
|
||||
check(not bool(drop2.get("first", true)) and int(drop2.get("amount", 0)) == int(row1.get("repeat_kill", -1)),
|
||||
"再通 L1:{first:false, amount=repeat_kill}(首杀标记置位后转小额)")
|
||||
var d3: Dictionary = mp.settle_adventure({"result": "failed", "cleared": false, "levels_cleared": 1,
|
||||
"stopped_at": 2, "keystone_total": 3, "master_seed": 13})
|
||||
"stopped_at": 2, "keystone_total": int(row1.get("repeat_kill", 0)),
|
||||
"keystone_drops": [{"level": 1, "first": false, "amount": int(row1.get("repeat_kill", 0))}],
|
||||
"master_seed": 13})
|
||||
check(not bool(d3["boss_drops"][0].get("first", true)) and int(d3["boss_drops"][0].get("amount", 0)) == int(row1.get("repeat_kill", -1)),
|
||||
"重复杀第三轮恒定不递减(验收5——无衰减曲线)")
|
||||
# 混合归因:通关两关 → L1 重复 + L2 首杀
|
||||
# 混合归因:通关两关 → L1 重复 + L2 首杀(明细随摘要逐笔归档)
|
||||
var d4: Dictionary = mp.settle_adventure({"result": "failed", "cleared": false, "levels_cleared": 2,
|
||||
"stopped_at": 0, "keystone_total": 4, "master_seed": 14})
|
||||
"stopped_at": 0, "keystone_total": int(row1.get("repeat_kill", 0)) + int(row2.get("first_kill", 0)),
|
||||
"keystone_drops": [{"level": 1, "first": false, "amount": int(row1.get("repeat_kill", 0))},
|
||||
{"level": 2, "first": true, "amount": int(row2.get("first_kill", 0))}],
|
||||
"master_seed": 14})
|
||||
check(not bool(d4["boss_drops"][0].get("first", true)) and bool(d4["boss_drops"][1].get("first", false))
|
||||
and int(d4["boss_drops"][1].get("amount", 0)) == int(row2.get("first_kill", -1)),
|
||||
"两关通关逐关归因:L1 重复档 + L2 首杀档(first_kill 金额)")
|
||||
@@ -3457,7 +3568,8 @@ func _meta_settle_and_workshop() -> void:
|
||||
# ⑩ recent_summaries:元素含 boss_drops 副本、最新在前、cap 10
|
||||
var rs: Array = sv.recent_summaries()
|
||||
check(rs.size() >= 5 and String(rs[0].get("result", "")) != "" and rs[0].has("boss_drops")
|
||||
and int(rs[0].get("master_seed", -1)) == 15, "摘要最新在前 + 元素含 boss_drops 副本(复盘可归因——验收5)")
|
||||
and (rs[1] as Dictionary).has("keystone_drops")
|
||||
and int(rs[0].get("master_seed", -1)) == 15, "摘要最新在前 + 元素含掉落明细副本(复盘可归因——验收5)")
|
||||
var sv_c = _meta_new_save(_META_SMOKE_PATHS[4])
|
||||
var mp_c = load("res://core/meta_progression.gd").new(_db, sv_c)
|
||||
for i in 12:
|
||||
@@ -3603,8 +3715,8 @@ func _smoke_save() -> void:
|
||||
fm.store_string(JSON.stringify({"version": 0, "keystone_old": 12}))
|
||||
fm.close()
|
||||
check(f7.load_from_disk() and f7.keystone_balance() == 12, "v0 旧档经迁移链入账(keystone_old 12 → 余额 12)")
|
||||
check(f7.write_to_disk() and int(JSON.parse_string(FileAccess.open(paths[5], FileAccess.READ).get_as_text()).get("version", -1)) == 2,
|
||||
"迁移链 0→1(测试钩)→2(生产) 写盘 version == 2(v2=断点域出生,迭代 8 卡①)")
|
||||
check(f7.write_to_disk() and int(JSON.parse_string(FileAccess.open(paths[5], FileAccess.READ).get_as_text()).get("version", -1)) == 3,
|
||||
"迁移链 0→1(测试钩)→3(生产) 写盘 version == 3(v3=断点附随件扩列,迭代 10 奖励重构)")
|
||||
|
||||
# ⑧ 域界干净:meta 永久进度域不含局内资源键(行动点/RAP/GP/道具库存/生命账本不入档——契约 2;
|
||||
# 迭代 8 卡①修订:扫描收窄到 meta 域——断点域=冒险进行中瞬态快照,生命/GP 按拍板面入域豁免)
|
||||
@@ -3632,9 +3744,11 @@ func _smoke_save() -> void:
|
||||
var st9: Dictionary = autoplay.run_adventure(_db, 42, 900, 400)
|
||||
var f9 = sm_script.new(paths[8])
|
||||
f9.apply_adventure_summary(st9.get("summary", {}))
|
||||
var kpl := int(_db.adventure.keystone_per_level)
|
||||
var ks_run := 0
|
||||
for li in range(1, int(_db.level.rows.size()) + 1):
|
||||
ks_run += int(_db.meta_boss_keystone_row(li).get("first_kill", 0))
|
||||
check(String(st9.get("result", "")) == "completed", "摘要集成:冒险驱动满配通关(seed 42)")
|
||||
check(f9.keystone_balance() == 3 * kpl, "集成钥石余额 = 3 × keystone_per_level(%d)" % f9.keystone_balance())
|
||||
check(f9.keystone_balance() == ks_run, "集成钥石余额 = ΣBoss 首杀行值(%d——逐杀掉落全源)" % f9.keystone_balance())
|
||||
check(f9.clear_count(1) == 1 and f9.clear_count(2) == 1 and f9.clear_count(3) == 1, "集成通关次数表全 1")
|
||||
check(f9.boss_first_kill_marked(1) and f9.boss_first_kill_marked(2) and f9.boss_first_kill_marked(3),
|
||||
"集成三关首杀标记齐")
|
||||
@@ -3643,11 +3757,11 @@ func _smoke_save() -> void:
|
||||
var st9b: Dictionary = autoplay.run_adventure(_db, 42, 900, 400)
|
||||
f9.apply_adventure_summary(st9b.get("summary", {}))
|
||||
check(f9.clear_count(1) == 2 and f9.clear_count(2) == 2 and f9.clear_count(3) == 2, "第二局后通关次数全 2")
|
||||
check(f9.keystone_balance() == 6 * kpl, "第二局后余额翻倍(6 × keystone_per_level)")
|
||||
check(f9.keystone_balance() == 2 * ks_run, "第二局后余额翻倍(2 × Σ首杀——模拟驱动恒首杀口径)")
|
||||
check(f9.recent_summaries().size() == 2, "集成 recent 长度 2")
|
||||
check(f9.write_to_disk(), "集成写盘成功")
|
||||
var r9 = sm_script.new(paths[8])
|
||||
check(r9.load_from_disk() and r9.keystone_balance() == 6 * kpl and r9.clear_count(3) == 2
|
||||
check(r9.load_from_disk() and r9.keystone_balance() == 2 * ks_run and r9.clear_count(3) == 2
|
||||
and r9.recent_summaries().size() == 2 and r9.boss_first_kill_marked(3),
|
||||
"集成写盘读回全量保持")
|
||||
|
||||
@@ -3698,9 +3812,9 @@ func _smoke_save() -> void:
|
||||
fold.store_string("{\"version\": 1, \"meta\": {\"keystone_balance\": 4}}")
|
||||
fold.close()
|
||||
check(fo.load_from_disk() and fo.keystone_balance() == 4 and not fo.has_breakpoint(),
|
||||
"v1 旧档迁移 v2:断点域补空(合法空态,无断点域不炸)")
|
||||
check(fo.write_to_disk() and int(JSON.parse_string(FileAccess.open(paths[9], FileAccess.READ).get_as_text()).get("version", -1)) == 2,
|
||||
"v1 旧档迁移后写盘 version == 2")
|
||||
"v1 旧档迁移 v3:断点域补空(合法空态,无断点域不炸)")
|
||||
check(fo.write_to_disk() and int(JSON.parse_string(FileAccess.open(paths[9], FileAccess.READ).get_as_text()).get("version", -1)) == 3,
|
||||
"v1 旧档迁移后写盘 version == 3")
|
||||
|
||||
# 段尾清理(测试文件绝不残留)
|
||||
for p in stale:
|
||||
|
||||
Reference in new issue
Block a user