From 3333187351a46bd558a36495ff139b531184201e Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Mon, 12 Oct 2026 00:28:22 +0800 Subject: [PATCH] =?UTF-8?q?feat(close):=20=E8=BF=AD=E4=BB=A320=20T5+T8+T9+?= =?UTF-8?q?T10=20C=E6=AE=B5=E6=94=B6=E5=B0=BE=EF=BC=88=E4=B8=93=E9=95=BF?= =?UTF-8?q?=E5=AE=9E=E8=A3=85=E6=89=B9=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit T5 Boss 兑付面换代:通关渠道三选一改按档配比加权抽取(无放回加权抽 boss_feat_weight 100/45/15/5 provisional 归调参批——奇物.md v3.1 rarity 语义约束兑付面);信号注释换代 「提升三选一」→「专长三选一」(V11 正名);V12 频带断言(300 停点档位频率出带±15pp)。 T8 MC 对账适配:formed 画像 14 旧件→五事件族 14 新件(开格 4/插旗 3/踩雷 4/节奏 2/战斗 1); 新基线取证:normal/off 100 局 71000 档 通关 0%(master 对照同为 0%=零配基线本相), 止步分布 90/10→93/7 可归因曝光槽候选构成;同种子重放 30 局逐位复现 ✓(V15)。 T9 迁出收尾:treasure_items 删 venom_edge/flag_burst 两行(同调目录收窄);schema 词面 venom_rider/flag_burst 摘除(EFFECT_KINDS/MAGNITUDE_KINDS);rewrite_board 效果类型除名 (净化工兵退役——V14 连带清理闭环);V13 断言入 smoke。 T10 空配置验收闸:三选一池空 skip_empty_curio 消化+非空拒绝反例+商店空货架合法态断言。 测试:pool_case_d 17 checks(+V12/T10)/feat_vouchers 23 checks(+V16 断点往返); 冒烟 864/0+run_all 21 套全绿。 --- game/core/adventure_runner.gd | 31 ++++++++++++- game/core/effect_engine.gd | 6 +-- game/data/schemas/adventure_table.gd | 11 +++++ game/data/schemas/treasure_item_table.gd | 4 +- game/data/tables/treasure_items.tres | 53 ---------------------- game/smoke/adventure_monte_carlo.gd | 11 +++-- game/smoke/smoke_all.gd | 2 + game/tests/unit/core/feat_vouchers_test.gd | 18 ++++++++ game/tests/unit/core/pool_case_d_test.gd | 38 ++++++++++++++++ 9 files changed, 111 insertions(+), 63 deletions(-) diff --git a/game/core/adventure_runner.gd b/game/core/adventure_runner.gd index 958827fe..977c47b0 100644 --- a/game/core/adventure_runner.gd +++ b/game/core/adventure_runner.gd @@ -41,7 +41,7 @@ class_name AdventureRunner extends RefCounted signal level_started(level_index: int, display_name: String) # 每关开始(含冒险首关)——呈现层重取 runner()、刷新图条与层名行 -signal curio_choice_presented(payload: Dictionary) # 提升三选一停点(非末关通关恰一次;迭代 10 大奖励重构——big_reward_paid 退役)——{level_index, candidates, heal_gained, hp_before, hp_after, keystone_gained, keystone_total, gp, aux_cleared} +signal curio_choice_presented(payload: Dictionary) # 专长三选一停点(非末关通关恰一次;迭代 20 T5 换代正名——旧「提升三选一」退役,通道白名单后候选恒专长+回血在载荷)——{level_index, candidates, heal_gained, hp_before, hp_after, keystone_gained, keystone_total, gp, aux_cleared} signal curio_resolved(result: Dictionary) # 三选一兑付完成(pick_curio/替换受理后恰一次)——{picked, value, hp_before, hp_after, level_index} signal treasure_replace_presented(items: Array, incoming: Dictionary) # 同调位满位替换子交互(pick_curio 道具件满位时)——items=现有持有件记录、incoming=新件记录 signal adventure_finished(summary: Dictionary) # 冒险结算(completed/failed/abandoned)——summary 五件事:打穿没有/通关几关/止步第几关/钥石合计/主种子(+additive keystone_drops 迭代 10) @@ -872,6 +872,35 @@ func _draw_curio_candidates(channel: String = "reward_node") -> Array: var t: Dictionary = tok as Dictionary return _curio_cap_ok(String(t.get("kind", "")), String(t.get("feat_id", "")))) var rg: RandomNumberGenerator = rng.open_stream("reward_pick") + if channel == "level_clear": + # 迭代 20 T5——Boss 渠道按档配比加权抽取(无放回加权抽:逐席按 boss_feat_weight 掷点, + # 身份去重照旧;rarity 语义约束兑付面——奇物.md v3.1,配比数值归调参批) + var out_w: Array = [] + var seen_w: Array = [] + var want_w := int(db.adventure.curio_candidate_count) + while out_w.size() < want_w and not pool.is_empty(): + var total_w := 0 + for tok_w in pool: + total_w += db.adventure.boss_feat_weight(String((tok_w as Dictionary).get("tier", ""))) + if total_w <= 0: + break + var roll_w := rg.randi_range(1, total_w) + var pick_i := 0 + for i_w in pool.size(): + roll_w -= db.adventure.boss_feat_weight(String((pool[i_w] as Dictionary).get("tier", ""))) + if roll_w <= 0: + pick_i = i_w + break + var tok_p: Dictionary = pool[pick_i] + var ident_p := _token_identity(tok_p) + if seen_w.has(ident_p): + pool.remove_at(pick_i) # 同身份已出——剔牌重掷(无放回) + continue + seen_w.append(ident_p) + var cand_p := {"kind": String(tok_p.get("kind", "")), "tier": String(tok_p.get("tier", "")), "feat_id": String(tok_p.get("feat_id", ""))} + out_w.append(cand_p) + pool.remove_at(pick_i) + return out_w for i in pool.size(): var j := rg.randi_range(i, pool.size() - 1) var tmp = pool[i] diff --git a/game/core/effect_engine.gd b/game/core/effect_engine.gd index bdd71b5e..ca8f8f63 100644 --- a/game/core/effect_engine.gd +++ b/game/core/effect_engine.gd @@ -32,7 +32,7 @@ const MINE_EVENTS := ["dig_reveal", "dig_paid", "flag_hit", "mine_hit", "flag_se const EFFECT_KINDS := ["add_ticket_rider", "modify_ticket", "damage_mult", "open_ticket", "attack_bonus", "attack_segments", "yield_mult", "spawn_spell", "overflow", "grant_material", "grant_ap", "grant_gp", "bias_wallet", "shield", "spike", "apply_status", "counter_add", - "counter_spend", "rewrite_board", "reveal_tiles", "remove_mines", "refresh_pick", + "counter_spend", "reveal_tiles", "remove_mines", "refresh_pick", "bias_rewards", "preview", # 迭代 20 专长实装批 T1 扩签六型(奇物.md 基础件表 27 件声明清单——落法映射 commit 留痕): # heal=回复生命〔血蜜/战地包扎〕;ac_bonus=护甲加值累积器〔封印桩本轮/石肤本场 cap〕; @@ -42,8 +42,8 @@ const EFFECT_KINDS := ["add_ticket_rider", "modify_ticket", "damage_mult", "open # target_attack_bonus=单位标记加伤〔处刑台——敌血量阈值挂单位字典,主角攻击面通用消费〕 "heal", "ac_bonus", "damage_reduce", "bonus_delta", "blast_scaled", "preview_area", "target_attack_bonus"] - # 注:rewrite_board 保留至本批 T3——老 27 行 purge_sapper(净化工兵)仍在表,删行同批除名 - # (迭代 20 卡 V14 连带清理;编排器消费分支已先行摘除=命中零消费 no-op 过渡) + # 注:rewrite_board 已除名(迭代 20 T9——净化工兵随老 27 件退役,V14 连带清理; + # 编排器消费分支 T1 先行摘除) ## fact 条件词汇(封闭枚举——迭代 20 T1 扩签 9→17:payload 同名键直读面〔fact 取值序 payload ## 优先——effect_engine._fact_value 既有机制〕。new_resource_type=首开资源类型〔地质学家〕; diff --git a/game/data/schemas/adventure_table.gd b/game/data/schemas/adventure_table.gd index 9f9154d1..6a5261b9 100644 --- a/game/data/schemas/adventure_table.gd +++ b/game/data/schemas/adventure_table.gd @@ -187,6 +187,17 @@ func curio_cap(kind: String) -> int: return 99 +## Boss 专长三选一按档配比加权(迭代 20 T5——奇物.md v3.1 rarity 语义约束「候选按档配比或 +## 加权抽取」兑付面;权重 provisional 归调参批——普通打底/优秀期待/稀有稀缺/传说头奖形态) +func boss_feat_weight(tier: String) -> int: + match tier: + TIER_COMMON: return 100 + TIER_EXCELLENT: return 45 + TIER_RARE: return 15 + TIER_LEGENDARY: return 5 + return 1 + + ## 档位判读序(MC 策略 316-f——1 级最稀有:legendary > rare > excellent > common;并列同档取最左由调用方稳定序保证) func tier_rank(tier: String) -> int: match tier: diff --git a/game/data/schemas/treasure_item_table.gd b/game/data/schemas/treasure_item_table.gd index ee6d9f11..36da1597 100644 --- a/game/data/schemas/treasure_item_table.gd +++ b/game/data/schemas/treasure_item_table.gd @@ -41,7 +41,7 @@ const DIMENSIONS := ["info", "risk", "action", "plan", "counter"] # 内容数据驱动扩展批 T2 挂账:主动件 kind 词族换代(reveal_tiles/preview/grant_ap/remove_mines/ # refresh_pick——效果引擎 ACTIVE_KINDS 同名闭集),被动件 effect_kinds 词族随 T4 秘宝迁移退役 # (passive 行改 triggers 事件订阅形态);旧词保留共存至迁移完成。 -const EFFECT_KINDS := ["mine_reveal", "reward_preview", "ap_grant", "venom_rider", "flag_burst", +const EFFECT_KINDS := ["mine_reveal", "reward_preview", "ap_grant", "mine_hit_halve", "reflect_damage", "ap_rounds_front", "ap_charge_safe", "cell_removal", "reward_weight_bias", "bonus_refresh", "reveal_tiles", "preview", "grant_ap", "remove_mines", "refresh_pick"] @@ -49,7 +49,7 @@ const TARGETS := ["none", "area_3x3", "board_wide"] const USE_PHASES := ["active_dig"] const FORMS := ["", "passive", "active_charged"] # magnitude 消费面(≥1 校验):grant_ap 与同调 N 值 kind;其余 kind 活行 magnitude 恒 0 -const MAGNITUDE_KINDS := ["ap_grant", "venom_rider", "flag_burst", "reflect_damage", "ap_rounds_front", +const MAGNITUDE_KINDS := ["ap_grant", "reflect_damage", "ap_rounds_front", "ap_charge_safe", "reward_weight_bias", "bonus_refresh", "grant_ap"] # 🔴 零裸 flat 结构护栏:行键白名单封闭(键出域=加载致命)——机制轴禁无条件属性加项的表结构承载 const KEY_WHITELIST := ["item_id", "display_name", "dimension_tags", "effect_kinds", "target", diff --git a/game/data/tables/treasure_items.tres b/game/data/tables/treasure_items.tres index 650f4270..056e2ed9 100644 --- a/game/data/tables/treasure_items.tres +++ b/game/data/tables/treasure_items.tres @@ -73,59 +73,6 @@ rows = [{ "slot_cost": 1, "charges_per_adventure": 2 }, { -"item_id": "venom_edge", -"display_name": "淬毒刃纹", -"dimension_tags": ["counter"], -"desc": "克制正面直伤+2", -"target": "none", -"magnitude": 2, -"use_phase": "active_dig", -"form": "passive", -"slot_cost": 1, -"triggers": [{ -"on": "clamp_check", -"when": [{ -"fact": "matchup", -"op": "eq", -"value": "pierce_vs_ward" -}], -"do": { -"kind": "modify_ticket", -"delta_from": "magnitude", -"mode": "clamp_bonus" -} -}, { -"on": "clamp_check", -"when": [{ -"fact": "matchup", -"op": "eq", -"value": "magic_vs_heavy" -}], -"do": { -"kind": "modify_ticket", -"delta_from": "magnitude", -"mode": "clamp_bonus" -} -}] -}, { -"item_id": "flag_burst", -"display_name": "标中殉爆", -"dimension_tags": ["action"], -"desc": "每标中一旗全体敌受2", -"target": "none", -"magnitude": 2, -"use_phase": "active_dig", -"form": "passive", -"slot_cost": 1, -"triggers": [{ -"on": "flag_hit", -"do": { -"kind": "open_ticket", -"value_from": "magnitude", -"channel": "blast" -} -}] -}, { "item_id": "nest_counter", "display_name": "惊巢反制", "dimension_tags": ["risk"], diff --git a/game/smoke/adventure_monte_carlo.gd b/game/smoke/adventure_monte_carlo.gd index 4608fbce..202c19ce 100644 --- a/game/smoke/adventure_monte_carlo.gd +++ b/game/smoke/adventure_monte_carlo.gd @@ -50,11 +50,14 @@ func _initialize() -> void: feat_mode = args[6] var feat_profile := {} if feat_mode == "formed": + # 迭代 20 T8 画像换代——旧 14 件随老 27 件退役;新画像=五事件族覆盖 14 件 + # (开格 4/插旗 3/踩雷 4/节奏 2/战斗 1——每族含跨事件账本件,账本件=攒层/计数面 MC 读数承载) feat_profile = {"mode": "on", "seed_ids": [ - "arcane_ember", "mana_weaver", "pierce_shot", "chain_arrows", - "miner_stubborn", "miner_hammer", "greedy_lantern", "vein_intuition", - "mine_atlas", "nest_hunter", "armory_prep", "iron_wall", - "ley_harvest", "thorn_weaver"]} + "twin_pick", "geologist", "hard_hat", "frontier_miner", + "seal_echo", "seal_smith", "seal_stake", + "pain_memory", "trap_instinct", "chain_traps", "blood_honey", + "last_drop", "market_close", + "execution_scholar"]} elif feat_mode == "ab_a": feat_profile = {"mode": "off", "tree_rules": ["posture_first_strike"]} elif feat_mode == "ab_b": diff --git a/game/smoke/smoke_all.gd b/game/smoke/smoke_all.gd index acfa6111..41079223 100644 --- a/game/smoke/smoke_all.gd +++ b/game/smoke/smoke_all.gd @@ -654,6 +654,8 @@ func _smoke_feats() -> void: # ② 旧 27 件退役钉面(V2——arcane_ember 系旧件表首行;全仓 grep 零残留归批末静态核查) check(_db.feats.row_by_id("arcane_ember").is_empty() and _db.feats.row_by_id("magma_body").is_empty(), "旧 27 件已退役:arcane_ember/magma_body 表内零残留〔专长批 V2〕") + check(_db.treasure_items.row_by_id("venom_edge").is_empty() and _db.treasure_items.row_by_id("flag_burst").is_empty(), + "迁出收尾:treasure_items 无 venom_edge/flag_burst 行(同调目录自然收窄)〔专长批 V13〕") # ③ 契约面静态钉扎(V18/V19/V21/V22——人工核对 V20/V21 裁决面以外的可机检半) var kinds_ok := true for t in a_r0._curio_deck: diff --git a/game/tests/unit/core/feat_vouchers_test.gd b/game/tests/unit/core/feat_vouchers_test.gd index 5cbc800a..189b2ef0 100644 --- a/game/tests/unit/core/feat_vouchers_test.gd +++ b/game/tests/unit/core/feat_vouchers_test.gd @@ -86,4 +86,22 @@ func _init() -> void: Kit.check_quiet(a.pick_curio("ap", "rare") and int(a.bonus.get("ap", 0)) == ap_bonus_before + int(db.adventure.curio_value("ap", "rare")), "行动点上限归属性轴:ap 兑付写加算区第四轴(bonus.ap 承载)〔专长批 V10〕") + # V16——断点扩容往返:新账本随断点快照/续玩回填(breakpoint_resume_test P5 同构) + var sm = load("res://core/save_manager.gd").new("user://_feat_vouchers_bp.json") + sm.load_from_disk() + a.adventure_counters = {"pain_layers": 3, "twin_digs": 5} + a._refresh_stock = 2 + a._battery_pending = 1 + a.set_save_manager(sm) + a._on_level_finished({"result": "victory", "hp": 10, "node_index": 1, "node_id": 1, "node_type": "shop"}) + var bp16: Dictionary = sm.breakpoint_snapshot() + Kit.check_quiet(int((bp16.get("adventure_counters", {}) as Dictionary).get("pain_layers", -1)) == 3 + and int(bp16.get("refresh_stock", -1)) == 2 and int(bp16.get("battery_pending", -1)) == 1, + "V16:新账本三件入断点快照(adventure_counters/refresh_stock/battery_pending)〔专长批 V16〕") + var b2 = load("res://core/adventure_runner.gd").new(db, load("res://core/rng_service.gd").new()) + Kit.check_quiet(b2.resume_adventure(bp16.duplicate(), {}), "V16 前置:续玩受理") + Kit.check_quiet(int(b2.adventure_counters.get("pain_layers", -1)) == 3 and b2.refresh_stock() == 2 and b2.battery_pending() == 1, + "V16:续玩回填逐项一致(计数恢复——跨战账不丢)〔专长批 V16〕") + sm.clear_breakpoint() + quit(Kit.finish("feat_vouchers")) diff --git a/game/tests/unit/core/pool_case_d_test.gd b/game/tests/unit/core/pool_case_d_test.gd index 995b3555..5f757ab0 100644 --- a/game/tests/unit/core/pool_case_d_test.gd +++ b/game/tests/unit/core/pool_case_d_test.gd @@ -27,6 +27,26 @@ func _init() -> void: var c = load("res://core/adventure_runner.gd").new(db, load("res://core/rng_service.gd").new()) check_v8(c, db) + # V12——Boss 三选一按档配比加权抽取(MC 同种子批量抽样档位频率出带;权重 provisional + # 100/45/15/5 ×表内件数 12/9/8/0 → 期望占比 ≈ 69/26/4/0,带 ±15pp 容差) + var m = load("res://core/adventure_runner.gd").new(db, load("res://core/rng_service.gd").new()) + check_v12(m, db) + + # T10——空配置验收闸(三选一池空兜底/商店空货架) + var e = load("res://core/adventure_runner.gd").new(db, load("res://core/rng_service.gd").new()) + e.start_adventure(501, {"first_script": true}) + e._curio_pending = {"candidates": []} + Kit.check_quiet(e.skip_empty_curio() and e._curio_pending.is_empty(), + "空配置闸:三选一池空=停点整体跳过(skip_empty_curio 消化)〔专长批 T10〕") + e._curio_pending = {"candidates": [{"kind": "gp", "tier": "common"}]} + Kit.check_quiet(not e.skip_empty_curio(), + "空配置闸反例:非空停点拒绝跳过(零自动推进契约不破)〔专长批 T10〕") + var lr = load("res://core/level_runner.gd").new(db, load("res://core/rng_service.gd").new()) + lr.set_shop_pool([], Callable(), Callable(), Callable()) + lr._shop_draw_shelf() + Kit.check_quiet((lr.shop_bp_shelf() as Array).is_empty(), + "空配置闸:商店空货架=合法态(牌库空=零扰动不崩)〔专长批 T10〕") + quit(Kit.finish("pool_case_d")) @@ -118,6 +138,24 @@ func check_v6(b, db) -> void: Kit.check_quiet(unchanged, "只刷出不计:抽签不改已拿账〔专长批 V6〕") +func check_v12(m, db) -> void: + if not m.start_adventure(401, {"first_script": true}): + Kit.check_quiet(false, "V12 前置:冒险启动") + return + var tier_n := {"common": 0, "excellent": 0, "rare": 0, "legendary": 0} + var total := 0 + for i in 300: + for t in m._draw_curio_candidates("level_clear"): + tier_n[String(t.get("tier", ""))] = int(tier_n.get(String(t.get("tier", "")), 0)) + 1 + total += 1 + Kit.check_quiet(total == 900, "V12 前置:300 停点 ×3 候选=900 席(实 %d)" % total) + var p_common := float(tier_n["common"]) / float(total) + var p_excellent := float(tier_n["excellent"]) / float(total) + var p_rare := float(tier_n["rare"]) / float(total) + Kit.check_quiet(p_common > 0.54 and p_common < 0.84 and p_excellent > 0.11 and p_excellent < 0.41 and p_rare >= 0.0 and p_rare < 0.19 and tier_n["legendary"] == 0, + "Boss 档配比频率出带(common %.2f/excellent %.2f/rare %.2f/legendary 0——权重 100/45/15/5×件数 12/9/8/0)〔专长批 V12〕" % [p_common, p_excellent, p_rare]) + + func check_v8(c, db) -> void: if not c.start_adventure(203, {"first_script": true}): Kit.check_quiet(false, "V8 前置:冒险启动")