## VERTICAL SLICE - NOT FOR PRODUCTION ## 统一随机流单点:唯一主种子+派生+全局取数计数器,全玩法取数必经 open_stream(垂直切片验证:同种子全程逐位复现)。 ## 玩法代码禁止 randomize()/自建 RandomNumberGenerator;重播种仅许全局计数器归零时(冒险中途重播种击穿同冒险无碰撞前提)。 ## open_stream 用途标签封闭词表 PURPOSE_TAGS——新用途=改代码扩签;断点续玩快照契约=主种子+counter()。 class_name RngServiceCore extends Node # 用途标签封闭词表(流名清单封闭;新增用途走扩签通道 = 改代码 + 评审) const PURPOSE_TAGS := ["board_gen", "reward_roll", "encounter", "armor_roll", "behavior_roll", "variant_place", "gp_drop", "node_map", "reward_pick", "shop_draw", "fx_reveal"] # node_map = 关卡节点图生成 v0.4 扩签;reward_pick = 提升三选一加权洗牌扩签(每关大奖励抽签恰一 scope:E-S weight-key 每候选一取,标签不参与派生只进事件日志,派生/黄金向量零改动);shop_draw = 商店房货架抽签扩签(每次抽 3〔进店+每次买后重抽〕恰一 scope,与 reward_pick 分签防取数序互扰;shop=off 零消费零扰动);fx_reveal = 专长订阅类揭示效果掷格(见血开窍/斩首学者等「揭示 1 格」,与 reward_pick 分签防取数序互扰;只揭真格=安全格信息红线承载) # 分段表达式书写(数据配置 R3 实现级规约——禁 ≥2^63 完整字面量) const GOLDEN_RATIO := (0x9E3779B9 << 32) | 0x7F4A7C15 # 0x9E3779B97F4A7C15 const SINGLE_SALT := (0xA0761D64 << 32) | 0x78BD642F # 0xA0761D6478BD642F(统一单盐——沿用并流前 board_gen 盐) # R3 混淆终末函数常量(splitmix64 finalizer 变体——互异奇常数 ≠ 乘数;全程 OUT_MASK 63 位掩码保持 # 有符号 int64 移位语义与参考实现逐位一致,见 _mix64 注记) const MIX_MUL_A := (0xBF58476D << 32) | 0x1CE4E5B9 # 0xBF58476D1CE4E5B9 const MIX_MUL_B := (0x94D049BB << 32) | 0x133111EB # 0x94D049BB133111EB const OUT_MASK := 0x7FFFFFFFFFFFFFFF const INT64_MIN := -9223372036854775808 # master_seed 只读出口(E5 封装收口):写入只经 set_master_seed(计数器全零断言 # 不可绕过);外部读 master_seed 供 存档序列化 快照契约观测。 var _master_seed: int = -1 # -1 = 未播种(取数即致命,EC7) var master_seed: int: get: return _master_seed var _counter: int = 0 # 全局取数计数器(唯一账本——快照契约 = 主种子 + 本计数器) # 违约通道(未知用途标签/未播种取数两分支共用)——缺省空 Callable 退化 push_error+assert(现行为逐字保持); # 注入后供负面套件断言「handler 被调用」(assert 只中断本函数,debug 下两态不可判别——记录位是唯一可观测面)。 var violation_handler: Callable = Callable() # 事件日志(丙案新增):每冒险一会话,逐次取数落一行(scope/用途标签/派生种子)——不致命、失败静默降级 var _log_path := "" var _log_fa: FileAccess = null var _log_warned := false # 纯函数(测试/预览/日志回放入口,不推计数器)——单盐单参数 func derive_at(scope: int) -> int: assert(scope >= 0, "负 scope 与 scope+2^63 经输出掩码别名(禁止)") var t: int = _master_seed + scope * GOLDEN_RATIO # wrap64 自然回绕——禁 float 中间量/禁 %/禁主动修复 return _mix64(SINGLE_SALT ^ t) # R3 派生终末混淆(splitmix64 finalizer 变体):裸派生种子直接作 # PCG 初始状态时,「连续小整数 master × 头部 scope 窗口」的面值彩票会被短局复用放大进聚合统计 # (30 连续种子 × 头部窗口 × 40 局复用 → 18 点聚合 +57%);此混淆使任何窄种子窗 # 表现如随机抽样。纯函数、逐 scope 复放语义不变;全程 OUT_MASK 63 位掩码——有符号 int64 的 >> # 为算术移位,63 位正值域内与逻辑移位逐位一致,这是与参考实现(Python 无符号模型)对齐的关键。 func _mix64(z: int) -> int: z = (z + GOLDEN_RATIO) & OUT_MASK z = (z ^ (z >> 30)) * MIX_MUL_A & OUT_MASK z = (z ^ (z >> 27)) * MIX_MUL_B & OUT_MASK return (z ^ (z >> 31)) & OUT_MASK func set_master_seed(seed_val: int) -> void: if seed_val == INT64_MIN: push_error("[RngService] master_seed = INT64_MIN 致命(abs 恒等回绕,与 0 静默别名)") assert(false, "INT64_MIN 致命") return if seed_val < 0: seed_val = -seed_val push_warning("[RngService] master_seed 负输入 abs 归一化: %d" % seed_val) assert(_counter == 0, "冒险中途重播种会击穿同冒险无碰撞前提(仅许全局计数器归零时调用)") _master_seed = seed_val _counter = 0 # 切片登记:冒险开始重播种入口(正式侧调用权归 冒险结构;切片主菜单 = 冒险起点)—— # 重播种同时开本次冒险的事件日志会话。 func new_adventure(seed_val: int) -> void: _counter = 0 set_master_seed(seed_val) _open_log_session(_master_seed) # 断点续玩恢复入口(快照契约「主种子+计数器」的回填面,调用权归 冒险结构): # 回填后首个 open_stream(scope=counter_val) 的派生种子与中断前逐位一致——续玩从断点号位精确 # 续接原时间线(停点前已消耗的号位不重耗,停点后取数逐位可复现)。仅续玩路径调用;模拟/冒烟 # 零触达(MC 对拍零扰动)。 func restore_adventure(seed_val: int, counter_val: int) -> void: _counter = 0 set_master_seed(seed_val) _counter = maxi(0, counter_val) _open_log_session(_master_seed) # 纯流面(节点图生成器核心批 卡① 契约 1 新增):结构生成专用取数面——(seed_val, scope) 的纯函数, # 不推全局计数器、不落事件日志、不读 _master_seed。同 (seed_val, scope) ⇒ 逐位同流, # 故生成器产图与号位无关(断点续玩在停点号位重生成同图的前提)。随机仍出自本服务(禁自建 RNG 红线不破—— # 本面是 derive_at 纯派生面的流化,与 open_stream 取号面同服务内分工,非另起第二套流)。 func pure_stream(seed_val: int, scope: int) -> RandomNumberGenerator: assert(seed_val != INT64_MIN, "pure_stream 种子 INT64_MIN 与 0 静默别名(禁止)") assert(scope >= 0, "负 scope 与 scope+2^63 经输出掩码别名(禁止)") var s: int = absi(seed_val) var t: int = s + scope * GOLDEN_RATIO # wrap64 自然回绕——与 derive_at 同语义 var rg := RandomNumberGenerator.new() rg.seed = _mix64(SINGLE_SALT ^ t) return rg # 取号门面:一次调用 = 一次全局 scope 取号(计数器读值 → 派生 → +1 → 落日志)。 # purpose 不参与派生——只作事件日志的用途标签(七流名保留为合法标签词表)。 func open_stream(purpose: String) -> RandomNumberGenerator: if not PURPOSE_TAGS.has(purpose): push_error("[RngService] 未知用途标签: %s" % purpose) if violation_handler.is_valid(): violation_handler.call(purpose) assert(false, "未知用途标签(标签词表封闭,EC12)") return null # release 剥 assert 后经调用方空引用即刻崩=fail-fast 传导(合法路径不可达) if _master_seed < 0: push_error("[RngService] 未设 master_seed 即取数(EC7——冒险结构 未接线的显式暴露)") if violation_handler.is_valid(): violation_handler.call(purpose) assert(false, "未播种取数致命(EC7)") return null # 同上(EC7 通道——release 下禁静默发号) var scope: int = _counter _counter = scope + 1 var rg := RandomNumberGenerator.new() var dseed: int = derive_at(scope) rg.seed = dseed _log_draw(purpose, scope, dseed) return rg # 只读计数器(存档序列化 快照契约的观测面——统一流后为全局单值) func counter() -> int: return _counter # ---- 事件日志(丙案:全事件落盘——号位失去事件语义后的单点定位承载面)---- # 会话入口:只由 new_adventure 开(直接 set_master_seed 的测试路径无日志会话,取数照常、不落盘) func _open_log_session(seed_val: int) -> void: _log_path = "user://logs/rng_log_%d.jsonl" % seed_val _log_fa = null _log_warned = false var dir := DirAccess.open("user://") if dir == null: push_warning("[RngService] 事件日志目录不可达(不致命,本次冒险不落盘)") return dir.make_dir_recursive("logs") if FileAccess.file_exists(_log_path): _log_fa = FileAccess.open(_log_path, FileAccess.READ_WRITE) if _log_fa != null: _log_fa.seek_end() # 同种子重跑追加 else: _log_fa = FileAccess.open(_log_path, FileAccess.WRITE) if _log_fa != null: _log_fa.store_line(JSON.stringify({"type": "session", "master_seed": seed_val})) func _log_draw(purpose: String, scope: int, dseed: int) -> void: if _log_fa == null: return _log_fa.store_line(JSON.stringify({"type": "draw", "scope": scope, "purpose": purpose, "dseed": dseed})) _log_fa.flush() if _log_fa.get_error() != OK and not _log_warned: _log_warned = true push_warning("[RngService] 事件日志写盘失败(不致命,本会话静默降级): %s" % _log_path)