## VERTICAL SLICE - NOT FOR PRODUCTION ## 格面资源身份层:install 期一次性预分配每格资源身份,授予期零随机、Ready 后只读无重掷(垂直切片验证:格面资源同种子逐位回放)。 ## roll_board 每板恰一次(Ready 相位再调 = double-roll 拒绝;换图 = 合法新批);禁在本类引入授予期随机或重掷。 ## 极性路由归战斗结算授予段——本类只提供 polarity_row 的(类型, 极性)→ 字典行查询面。 class_name RewardEconomy extends RefCounted var db: ConfigDb var rng: RngServiceCore var _phase := "unrolled" # unrolled → ready → retired var _eligible: Array[Dictionary] = [] # [{cell}] 行主序规范序 var _mapping := {} # Vector2i → reward_id(正向行身份——格面资源类型由 cell_type 承载) var _power_roll := {} # Vector2i → 区间行 roll 值(身份批内追加取号) var _clear_power := -1 # 清图槽区间行 roll 值(-1 = 定值行) var _granted := {} # Vector2i → 快照(重复受理幂等返回) var _clear_identity := "" var _clear_granted := false var _clear_snapshot := {} # 效果引擎(reward_roll 发射面;偏运硬币 bias_rewards 订阅面) var _fx: EffectEngine = null var _ctx: EffectContext = null func set_fx(fx: EffectEngine, ctx: EffectContext) -> void: _fx = fx _ctx = ctx func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void: db = db_ref rng = rng_ref # ---- install 期 roll(恰一次;Ready 相位再调拒绝)---- # eligible = 全部非封锁格(安全格 + 危险格,战斗结算 install 编排供给;封锁格不入清单) func roll_board(eligible: Array) -> bool: # 相位守卫:Ready 再调 = double-roll 拒绝(E5);Retired(换图后)= 合法新批重置(E6 roll 半) if _phase == "ready": push_warning("[地图战斗资源] roll_board 相位守卫拒绝(E5——double-roll 编排违约显式暴露)") return false if eligible.is_empty(): _fatal("roll_board 空资格清单(E1 兜底 fail-fast——排雷玩法 不变式④前置)") return false # 行主序规范化 + 去重(排序责任不外推给 战斗结算;排序键单点 BoardData.row_major_key) var seen := {} var list: Array[Dictionary] = [] for e in eligible: var c: Vector2i = e.cell if seen.has(c): push_warning("[地图战斗资源] 资格清单重复格已去重: %s" % str(c)) continue seen[c] = true list.append({"cell": c}) list.sort_custom(func(a, b): return BoardData.row_major_key(a.cell) < BoardData.row_major_key(b.cell)) _eligible = list _granted.clear() # 新批重置映射受理面(旧图快照消亡) _power_roll.clear() _clear_power = -1 _clear_granted = false _clear_snapshot = {} var stream: RandomNumberGenerator = rng.open_stream("reward_roll") # 恰一 scope 批(格批 + 清图批尾 + 区间行追加取号) # reward_roll 发射+施加(格面资源掷定、摇号前权重合成层: # bias_rewards 订阅命中合计=偏运权重加算(行 value_from magnitude 定影);零命中=原行集零扰动) var luck_bias := 0 if _fx != null: for h in _fx.dispatch(_ctx, "reward_roll", {"polarity": "positive", "cell_count": list.size()}): if String(h.get("kind", "")) == "bias_rewards": luck_bias += int(h.get("value", 0)) var cell_rows: Array[Variant] = _biased_cell_rows(luck_bias) # cell 池正向行 weight>0(偏运硬币装配时稀有正向 kinds 权重 +N——池结构零触碰) var clear_rows: Array[Variant] = _pool_rows("clear_default") # ① 身份批:每非封锁格恰一类型身份(雷安同池——同一行集同一权重分布) var draft := {} for e in list: draft[e.cell] = weighted_pick(cell_rows, stream.randi_range(0, _pool_weight(cell_rows) - 1)) # ② 清图槽批尾:同流同批判尾摇定清图奖励对象身份 var clear_rid := weighted_pick(clear_rows, stream.randi_range(0, _pool_weight(clear_rows) - 1)) # ③ 区间行 power roll(身份定稿后逐格恰一追加取号,定值行零取号) for cell in draft.keys(): var row: Dictionary[Variant, Variant] = _row_by_id(draft[cell]) if row.has("power_min"): _power_roll[cell] = stream.randi_range(int(row.power_min), int(row.power_max)) var clear_row: Dictionary[Variant, Variant] = _row_by_id(clear_rid) if clear_row.has("power_min"): _clear_power = stream.randi_range(int(clear_row.power_min), int(clear_row.power_max)) _mapping = draft _clear_identity = clear_rid _phase = "ready" return true # ---- 查询面(Ready 只读;未开格恒可查——秘宝 桥接结构前提)---- # 相位只读出口(诊断观测面——unrolled/ready/retired;消费方禁直读 _phase 下划线字段) func phase() -> String: return _phase # 静默映射查询(批量工具用——零 E19 诊断;语义 = query_identity 非 null 的布尔投影) func is_mapped(cell: Vector2i) -> bool: return _phase == "ready" and _mapping.has(cell) func query_identity(cell: Vector2i): if _phase != "ready": _diag("query_identity 非 Ready 相位") return null var rid: String = _mapping.get(cell, "") if rid == "": _diag("query_identity 清单外坐标(E19——未分配非错误态;封锁格不入清单)") return null var row: Dictionary[Variant, Variant] = _row_by_id(rid) return {"reward_id": rid, "cell_type": String(row.cell_type), "tier": int(row.tier)} # 身份完整快照(轮内刷新批次隔离用——facts 缓冲期捕获,授予期 mapping 可能已换批): # 返回与 grant 相同结构的快照(cell_type/display_name/power/effect_id/stacks 齐备), # 供 战斗结算 冻结授予直通 func query_full_snapshot(cell: Vector2i): if _phase != "ready": _diag("query_full_snapshot 非 Ready 相位") return null var rid: String = _mapping.get(cell, "") if rid == "": return null return _snapshot(_row_by_id(rid), int(_power_roll.get(cell, -1))) # ---- 受理面(战斗结算 编排期调用;E6 收口:失败统一返回 null)---- func grant(cell: Vector2i): if _phase != "ready": _diag("grant 非 Ready 相位(E4/E7)") return null if not _mapping.has(cell): _diag("grant 清单外坐标(E19)") return null if _granted.has(cell): _diag("grant 同格重复受理(E18 幂等兜底——受理恰一次归 战斗结算)") return _granted[cell] var snap := _snapshot(_row_by_id(_mapping[cell]), int(_power_roll.get(cell, -1))) _granted[cell] = snap return snap func grant_clear(): if _phase != "ready": _diag("grant_clear 非 Ready 相位") return null if _clear_granted: _diag("grant_clear 同批次重调(E18 幂等兜底)") return _clear_snapshot var row: Dictionary[Variant, Variant] = _row_by_id(_clear_identity) _clear_snapshot = _snapshot(row, _clear_power) _clear_granted = true return _clear_snapshot func retire_mapping() -> bool: if _phase != "ready": _diag("retire_mapping 非 Ready 相位(幂等受理族)") return false _phase = "retired" _mapping.clear() _power_roll.clear() _clear_power = -1 _clear_identity = "" return true # ---- 极性路由字典面(战斗结算 授予段消费):(类型, 极性)→ 格面池字典行(拷贝出口)---- func polarity_row(cell_type: String, polarity: String) -> Dictionary: for row in db.reward.rows: if String(row.get("pool_tag", "")) == "cell" \ and String(row.get("cell_type", "")) == cell_type \ and String(row.get("polarity", "")) == polarity: return row.duplicate() # E6:表行值拷贝出口——消费方禁改配置源 _fatal("极性字典缺行(八型×正负齐备为加载前提): %s %s" % [cell_type, polarity]) return {} # ---- F1 选行算子纯函数 ---- # 偏运硬币 N(reward_roll bias_rewards 订阅命中合计经 _biased_cell_rows 入参;0 = 未装配零扰动) # 稀有正向资源 kinds(底稿 §3——权重投影面,池结构零触碰) const LUCK_BIAS_KINDS := ["heal", "gp", "pierce", "magic"] # 图级偏斜旋钮(battle_orchestrator 装板决议注入,全局型行 # effect_value 语义=生存族权重下调百分比;0 = 中性 off 态。每板决议时显式重置/写入) var board_skew: int = 0 # 偏斜族(需求卡片 §二:进攻族等比上浮/生存族按 v% 下调/白名单豁免恒不动——判定键=cell_type; # 其余 cell_type(weaken/gp/action 白名单与未来扩型)= 豁免;负向行不入摇号已在 _rollable_rows 滤除) const SKEW_DOWN_KINDS := ["heal", "defense"] const SKEW_UP_KINDS := ["attack", "blast", "pierce", "magic"] # 偏运硬币权重投影(底稿 §3:只动池内权重、雷安仍同池同分布;luck_bias=0 逐位返回原行集——off 态零扰动) # 合成序:先经图级偏斜投影再叠件级加法——合成序锚=图级先、件级后 #(环境事实先定型、玩家构筑叠其上;两投影均为池内权重面,雷安同池与取号次数零变化) func _biased_cell_rows(bias: int) -> Array: var base: Array[Variant] = _skewed_cell_rows() if bias <= 0: return base var out: Array[Variant] = [] for row in base: if LUCK_BIAS_KINDS.has(String(row.get("cell_type", ""))): var biased: Dictionary[Variant, Variant] = row.duplicate() biased["weight"] = int(row.weight) + bias out.append(biased) else: out.append(row) return out # 图级资源面偏斜投影(整数最大余数法:生存族按 board_skew% 下调,下调量按进攻族 # 原权重比例整数重分配,合计恒等于原池合计 77;负向行与白名单三行逐位不动)。 # board_skew<=0 逐位返回原行集 = off 态零扰动第一断言:不进投影域、行集/行序/取号域与基线逐位同。 func _skewed_cell_rows() -> Array: if board_skew <= 0: return _rollable_rows() var base: Array[Variant] = _rollable_rows() var down: Array[Variant] = [] var up: Array[Variant] = [] for row in base: var k := String(row.get("cell_type", "")) if SKEW_DOWN_KINDS.has(k): down.append(row) elif SKEW_UP_KINDS.has(k): up.append(row) if down.is_empty() or up.is_empty(): return base # 结构缺族 = 无偏斜作用面(保守直通——池结构变更须走表加载校验另批) var v := board_skew # 下调总量 = round_half_up(Σ生存×v/100);逐行精确份额 w×v/100 最大余数取整(行序平局) var drops := _largest_remainder_split(down, v, 100, (2 * _pool_weight(down) * v + 100) / 200) # 上浮总量 = 下调总量(守恒),逐行精确份额 drop×w/Σ进攻 最大余数取整 var gains := _largest_remainder_split(up, drops_total(drops), _pool_weight(up), drops_total(drops)) var di := 0 var ui := 0 var out: Array[Variant] = [] for row in base: var k := String(row.get("cell_type", "")) var w := int(row.weight) if SKEW_DOWN_KINDS.has(k): w -= drops[di] di += 1 elif SKEW_UP_KINDS.has(k): w += gains[ui] ui += 1 assert(w > 0, "图级偏斜投影产生非正权重(池结构变更须走另批重审)") var skewed: Dictionary[Variant, Variant] = row.duplicate() skewed["weight"] = w out.append(skewed) assert(_pool_weight(out) == _pool_weight(base), "图级偏斜守恒破(偏斜后合计须等于原池)") return out func drops_total(amounts: Array[int]) -> int: var t := 0 for a in amounts: t += a return t # 最大余数整数分摊(行序平局——确定性):把 total 单位按精确份额 w_i×numer/denom 分到 rows # (floor 后余数单位依次给分数最大者,平局取行序在前)——Σ结果恒 = total static func _largest_remainder_split(rows: Array, numer: int, denom: int, total: int) -> Array[int]: var floors: Array[int] = [] var fracs: Array[int] = [] var used := 0 for row in rows: var exact: int = int(row.weight) * numer floors.append(int(exact / denom)) fracs.append(exact % denom) used += int(exact / denom) var order: Array[int] = [] for i in rows.size(): order.append(i) order.sort_custom(func(a: int, b: int) -> bool: if fracs[a] != fracs[b]: return fracs[a] > fracs[b] return a < b) var out: Array[int] = floors var rest: int = total - used assert(rest >= 0 and rest <= order.size(), "最大余数分摊余数越界(不应可达)") for j in rest: out[order[j]] += 1 return out func weighted_pick(rows: Array, r: int) -> String: var total := _pool_weight(rows) assert(r >= 0 and r < total, "weighted_pick 取号越域(纯函数域守卫)") var acc := 0 for row in rows: acc += int(row.weight) if r < acc: return String(row.reward_id) _fatal("weighted_pick 未命中(不应可达)") return "" # aux 解锁门控集(「解锁→进投放池」:未解锁 aux 行不入摇号池。 # 空集默认 = 全部 aux 不投放(白板/夹具态零投放——模拟读数零扰动;真实侧 main 组装工坊解锁集传入) var _treasure_unlocked := {} func set_treasure_unlocked(ids: Dictionary) -> void: _treasure_unlocked = ids func _rollable_rows() -> Array: # 摇号池 = cell 池正向行 weight>0(负向行纯字典不消费权重;封锁格不在本函数职责域——清单由调用方裁剪) var out: Array[Dictionary] = [] for row in db.reward.rows: if String(row.get("cell_type", "")) == RewardTable.TREASURE_ITEM_TYPE: var iid := String(row.get("effect_id", "")) if iid == "" or not _treasure_unlocked.has(iid): continue if String(row.get("pool_tag", "")) == "cell" and String(row.get("polarity", "")) == "positive" and int(row.weight) > 0: out.append(row) return out func _pool_rows(pool_tag: String) -> Array: var out: Array[Dictionary] = [] for row in db.reward.rows: if row.pool_tag == pool_tag and int(row.weight) > 0: out.append(row) return out func _pool_weight(rows: Array) -> int: var w := 0 for row in rows: w += int(row.weight) return w func _row_by_id(rid: String) -> Dictionary: for row in db.reward.rows: if row.reward_id == rid: return row return {} func _snapshot(row: Dictionary, rolled_power: int = -1) -> Dictionary: var snap := { "reward_id": String(row.reward_id), "cell_type": String(row.cell_type), "display_name": String(row.display_name), "power": int(row.power) if rolled_power < 0 else rolled_power, # 区间行 = 身份批 roll 值(E14 授予时快照口径) "tier": int(row.tier), "stacks": int(row.get("stacks", 1)), } if String(row.get("effect_id", "")) != "": snap["effect_id"] = String(row.effect_id) return snap func _diag(msg: String) -> void: push_warning("[地图战斗资源] " + msg) func _fatal(msg: String) -> void: push_error("[地图战斗资源/fatal] " + msg) assert(false, msg)