diff --git a/prototypes/dungeon-sweeper-vertical-slice/smoke/smoke_all.gd b/prototypes/dungeon-sweeper-vertical-slice/smoke/smoke_all.gd index 0e6e1d77..9bfdea57 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/smoke/smoke_all.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/smoke/smoke_all.gd @@ -284,7 +284,16 @@ func _smoke_adventure() -> void: var a8 = same["a"] var r8 = a8.runner() var gp0: int = int(r8.gp) - check(gp0 >= 4, "进店余额 ≥ hp_up L2 首价 4(%d)" % gp0) + var hp_price := -1 + for p in _db.shop.products: + if String(p.get("product_id", "")) == "hp_up": + hp_price = int(p.get("base_price", -1)) + var l2_price_bonus := 0 + for pb in _db.shop.price_level_bonus: + if int(pb.get("level_index", -1)) == 2: + l2_price_bonus = int(pb.get("bonus", 0)) + 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)) @@ -408,7 +417,12 @@ func _smoke_reward_identity_unit() -> void: var cs1: Dictionary = reward_economy.grant_clear() var cs2: Dictionary = reward_economy.grant_clear() check(String(cs1.cell_type) == "blast" and String(cs1.reward_id) == "clear_blast", "清图批尾身份 = clear_blast(clear_default 唯一活行)") - check(int(cs1.power) >= 24 and int(cs1.power) <= 40, "清图奖励 power ∈ [24,40](区间行 roll 值 %d)" % int(cs1.power)) + var cb_range := [-1, -1] + for row in _db.reward.rows: + if String(row.reward_id) == "clear_blast": + cb_range = [int(row.get("power_min", row.power)), int(row.get("power_max", row.power))] + check(int(cs1.power) >= int(cb_range[0]) and int(cs1.power) <= int(cb_range[1]), + "清图奖励 power ∈ 表区间 [%d, %d](roll 值 %d——配置推导)" % [cb_range[0], cb_range[1], int(cs1.power)]) check(JSON.stringify(cs1) == JSON.stringify(cs2), "grant_clear 同批次重调幂等") # 退役后受理拒绝 reward_economy.retire_mapping() @@ -459,6 +473,7 @@ func _smoke_flag_ap_exhausted() -> void: func _smoke_level() -> void: print("--- 关卡节点地图·线性推进(战×2→道具→战×2→恢复→Boss)---") var autoplay = load("res://smoke/level_autoplay.gd") + var hero_base := {"atk": int(_db.hero.atk), "hp": int(_db.hero.max_hp), "ac": int(_db.hero.armor_class)} # 配置基线快照——⑤ 回基线对比用(改表自动跟随,不再硬编码) # ① 满配通关路径(atk+200%/hp+100%——测试面板同源的属性覆盖) var win = autoplay.run_level(_db, 42, 1, 200, 100) check(win.result == "victory", "关卡·满配(atk+200/hp+100)通关(实测:%s @节点 %d)" % [win.result, win.defeat_node_index]) @@ -483,8 +498,10 @@ func _smoke_level() -> void: var rep2 = autoplay.run_level(_db, 777, 1, 100, 100, true) check(JSON.stringify(rep1.rows) == JSON.stringify(rep2.rows), "关卡回放断言:同种子两遍结算步骤流逐行相同") check(rep1.rows.size() > 0, "关卡步骤流非空(%d 行)" % rep1.rows.size()) - # ⑤ 属性覆盖恢复:模拟后 hero 表回基线(调整板不泄漏到后续会话;基线血线 20=Package B 第 2 轮拍板) - check(int(_db.hero.atk) == 2 and int(_db.hero.max_hp) == 20, "模拟后 hero 表恢复基线(atk=%d hp=%d)" % [int(_db.hero.atk), int(_db.hero.max_hp)]) + # ⑤ 属性覆盖恢复:模拟后 hero 表回基线(调整板不泄漏到后续会话;基线 = 函数入口的配置快照,改表自动跟随) + check(int(_db.hero.atk) == int(hero_base.atk) and int(_db.hero.max_hp) == int(hero_base.hp) \ + and int(_db.hero.armor_class) == int(hero_base.ac), + "模拟后 hero 表恢复基线(atk=%d hp=%d ac=%d——对配置快照)" % [int(_db.hero.atk), int(_db.hero.max_hp), int(_db.hero.armor_class)]) # ---- 护甲等级掷点批·换装护栏(2026-09-16 用户拍板:百分比减伤行整体退役换装 AC,无双轨)---- @@ -527,8 +544,9 @@ func _smoke_ac_swap_guard() -> void: wall_detail += " %s(%d)" % [eff_id, floor_v] check(wall_ok, "hero_defense 池 percent 行叠满不可触池底(墙最深%s > clamp_lo %d——单效果归零结构性不可达)" % [ wall_detail if wall_detail != "" else "(池内现无负向行)", int(_db.settlement.pool_clamp_lo)]) - # ③ 单位表 armor_class - check(int(_db.hero.armor_class) == 10, "主角 armor_class = 10(暂定基准——EV 期望减免 21.25%%)") + # ③ 单位表 armor_class:域内校验(配置推导——正式投值归怪物/难度批,改表不断锚) + check(int(_db.hero.armor_class) >= 0 and int(_db.hero.armor_class) <= 30, + "主角 armor_class 在域内(%d——暂定基准随表走)" % int(_db.hero.armor_class)) var ac_all := true for row in _db.enemy.rows: if not row.has("armor_class") or int(row.armor_class) != 0: @@ -1005,20 +1023,21 @@ func _auto_play_round(settlement, out: Dictionary) -> void: func _smoke_power_interval() -> void: print("--- 地图战斗资源·伤害量值区间(上阶/下阶随机)---") - # ① 表侧静态锚:区间行配对齐全、power = 中点(行动经济 条款①期望当量锚)——现行区间行 = heal_minor/clear_blast 两行 - # (Package B 第 2/4 轮 2026-09-22:heal_minor [1,1]→[2,2] 定值、clear_blast [8,18]→[24,40]——中点锚 power 随动) - var interval_rows := {"heal_minor": [2, 2], "clear_blast": [24, 40]} + # ① 表侧静态锚(配置推导——2026-09-23 重构,改表不再断锚):凡带 power_min/power_max 的行 = 区间行, + # 校验配对齐全(lo ≤ hi)+ power = floor 中点(行动经济 条款①期望当量锚);其余行必须零区间字段。 + # 行清单与区间值全部随表走——现行区间行 heal_minor/clear_blast 只是恰然,不再是锚。 + var interval_rows := {} # rid → [lo, hi](读表) var anchors_ok := true for row in _db.reward.rows: var rid := String(row.reward_id) - if interval_rows.has(rid): + if row.has("power_min") or row.has("power_max"): var lo: int = int(row.get("power_min", -1)) var hi: int = int(row.get("power_max", -1)) - if lo != interval_rows[rid][0] or hi != interval_rows[rid][1] or int(row.power) != (lo + hi) / 2: + interval_rows[rid] = [lo, hi] + if lo > hi or int(row.power) != (lo + hi) / 2: anchors_ok = false - elif row.has("power_min") or row.has("power_max"): - anchors_ok = false # 区间配对只许两行(其余全定值行) - check(anchors_ok, "区间行域与中点锚在位(heal_minor/clear_blast——其余行零区间字段)") + check(anchors_ok and interval_rows.size() >= 1, + "区间行配对齐全且 power = floor 中点(配置推导 %d 行:%s)" % [interval_rows.size(), str(interval_rows.keys())]) # ② 运行侧:24 板 roll——区间行快照 power 全域内 + 上下阶实际流动 + 定值行不受扰 var rng = load("res://core/rng_service.gd").new() rng.set_master_seed(20260914) @@ -1026,7 +1045,9 @@ func _smoke_power_interval() -> void: var eligible: Array = [] for i in range(6): eligible.append({"cell": Vector2i(i, 0)}) - var seen := {"heal_minor": {}, "clear_blast": {}} # rid → {roll 值集} + var seen := {} + for rid in interval_rows.keys(): + seen[rid] = {} # rid → {roll 值集}(区间行随表注册) var in_domain := true var fixed_ok := true for trial in range(24): @@ -1045,20 +1066,29 @@ func _smoke_power_interval() -> void: fixed_ok = false # 定值行快照 power 必须仍是表值 var cs: Dictionary = reward_economy.grant_clear() var cv := int(cs.power) - seen["clear_blast"][cv] = true - if cv < interval_rows["clear_blast"][0] or cv > interval_rows["clear_blast"][1]: + var c_rid := String(cs.reward_id) + if not seen.has(c_rid): + seen[c_rid] = {} + seen[c_rid][cv] = true + if interval_rows.has(c_rid) and (cv < interval_rows[c_rid][0] or cv > interval_rows[c_rid][1]): in_domain = false check(in_domain, "24 板全区间行快照 power ∈ [power_min, power_max](含清图槽)") check(fixed_ok, "定值行快照 power 恒等表值——区间批零外溢") - # 数值压缩批(2026-09-17):heal_minor 区间 [1,1] 退化为定值行——变异流动断言只适用 clear_blast; - # Package B 第 2 轮(2026-09-22):heal_minor 定值 1→2——改查 roll 恒等于定值 2。 - var variance_ok: bool = int(seen["clear_blast"].size()) >= 2 - var heal_fixed := true - for v in seen["heal_minor"].keys(): - if int(v) != 2: - heal_fixed = false - check(variance_ok, "上阶/下阶实际流动(clear_blast ≥2 个不同 roll 值)") - check(heal_fixed, "heal_minor 定值行 roll 恒 2([2,2] 零宽区间)") + # 定值行(区间 lo==hi 的行,如 heal_minor)roll 恒等表值;区间行(lo 0 and not (samples == 1 and seen[rid].has(lo)): + fixed_rows_ok = false + elif samples >= 2 and seen[rid].size() < 2: + variance_ok = false + check(variance_ok, "区间行上下阶实际流动(≥2 样本的区间行须 ≥2 个不同 roll 值)") + check(fixed_rows_ok, "定值行(lo=hi)roll 恒等表值") func _row_power(rid: String) -> int: @@ -3077,7 +3107,19 @@ func _smoke_nine_types_gp_stream() -> void: if String(row.get("reward_id", "")) == "gp_lose": gp_neg = int(row.weight) check(cell_rows == 18, "奖励字典 18 格(九型 × 正负——清图槽 clear_default 独立批尾不计)") - check(pos_weight == 78, "正向权重合计 78(heal_minor 2→4 后 76→78——Package B 第 2 轮)") + # 正向权重结构校验(配置推导,2026-09-23 重构):正向 cell 行权重 ≥1(必在池)、负向行权重恒 0(不入池)、 + # 合计即配置自身的和(改表自动跟随,不再硬编码合计值)。 + var pos_all_in_pool := true + var neg_all_zero := true + for row in _db.reward.rows: + if String(row.get("pool_tag", "")) != "cell": + continue + if String(row.get("polarity", "")) == "positive" and int(row.get("weight", 0)) < 1: + pos_all_in_pool = false + if String(row.get("polarity", "")) == "negative" and int(row.get("weight", 0)) != 0: + neg_all_zero = false + check(pos_all_in_pool and neg_all_zero, + "正向 cell 行权重 ≥1 全在池 / 负向行权重恒 0 不入池(正向合计 %d——配置推导)" % pos_weight) check(gp_pos == 2 and gp_neg == 0, "gp 型两行(金币袋 gp_gain 正向 weight 2 / 破财 gp_loss 负向 weight 0 不入池)") var rng = _fresh_rng(42) check(rng.derive_at("gp_drop", 0) == 6226476397888000011, "R3 向量 gp_drop m=42 scope=0(GP+商店房实施批——registry salt_gp_drop=0xD6E8FEB8665FD7F1 权威值)")