143 lines
8.4 KiB
GDScript
143 lines
8.4 KiB
GDScript
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
|
||
# Date: 2026-09-10
|
||
# RngService autoload(数据配置)——封闭命名流单点(Rule 5)。玩法代码禁止 randomize()/自建 RandomNumberGenerator。
|
||
# 实现 GDD:design/archive/gdd-2026-09-14/d2-data-config.md(系统 ID D2·数据配置——registry 追溯锚,命名规范见 CLAUDE.md)
|
||
# R3 派生公式(2026-09-19 掷点测量修复批 B 案改版,黄金向量已重登记):derive = mix64(SALT XOR wrap64(master + scope×GOLDEN))——splitmix64 finalizer 变体终末混淆,见 _mix64 注记;R2 旧向量随批作废
|
||
# 取号语义:全新实例首个 open_stream = scope 0(地图战斗资源 预推进术语的登记口径)。
|
||
extends Node
|
||
|
||
const STREAM_NAMES := ["board_gen", "reward_roll", "encounter", "armor_roll", "behavior_roll", "variant_place", "gp_drop"] # 2026-09-12 增 encounter(遭遇随机化批);2026-09-16 增 armor_roll(护甲等级掷点批)+ behavior_roll(敌人技能池批——开轮摇号,每敌派生独立子流,registry rng_service 条目同步);2026-09-19 增 variant_place(地形变异首批类型定稿批——变异布置流,一板恰一 scope);2026-09-21 增 gp_drop(GP+商店房实施批——战斗掉落掷定流,每战斗末恰一 scope,registry rng_service 条目随批同步)
|
||
|
||
# 分段表达式书写(数据配置 R3 实现级规约——禁 ≥2^63 完整字面量)
|
||
const GOLDEN_RATIO := (0x9E3779B9 << 32) | 0x7F4A7C15 # 0x9E3779B97F4A7C15
|
||
const SALT_BOARD_GEN := (0xA0761D64 << 32) | 0x78BD642F # 0xA0761D6478BD642F
|
||
const SALT_REWARD_ROLL := (0xE7037ED1 << 32) | 0xA0B428DB # 0xE7037ED1A0B428DB
|
||
const SALT_ENCOUNTER := (0x8EBC6AF0 << 32) | 0x9C88C6E3 # 0x8EBC6AF09C88C6E3(遭遇骰——registry stream_seed_derivation 权威值;旧值 0x5DEECE66D<<32 超 int64 被截断,黄金向量漂移)
|
||
const SALT_ARMOR_ROLL := (0xC2B2AE3D << 32) | 0x27D4EB2F # 0xC2B2AE3D27D4EB2F(护甲掷点流——互异奇常数 ≠ 乘数;黄金向量见冒烟/registry 同批登记)
|
||
const SALT_BEHAVIOR_ROLL := (0x85EBCA77 << 32) | 0x58C24E15 # 0x85EBCA7758C24E15(技能摇号流——互异奇常数 ≠ 乘数;黄金向量见冒烟/registry 同批登记)
|
||
# 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 SALT_VARIANT_PLACE := (0x9AE16A3F << 32) | 0x2FAD6C39 # 0x9AE16A3F2FAD6C39(变异布置流——互异奇常数 ≠ 乘数;地形变异首批类型定稿批 2026-09-20,黄金向量按 R3 公式登记)
|
||
const SALT_GP_DROP := (0xD6E8FEB8 << 32) | 0x665FD7F1 # 0xD6E8FEB8665FD7F1(战斗掉落流——互异奇常数 ≠ 乘数;GP+商店房实施批 2026-09-21,黄金向量按 R3 公式登记归冒烟/registry 同批)
|
||
# behavior_roll 派生向量(registry 同批登记):子流 scope = spawn_index × STRIDE + 单敌摇号序(0 起)。
|
||
# 每敌消费自己的 scope 段 → 单敌出招轨迹可独立复放、敌间互不穿插(proposal 约束二)。
|
||
const BEHAVIOR_SUBSTREAM_STRIDE := 1000000
|
||
const OUT_MASK := 0x7FFFFFFFFFFFFFFF
|
||
const INT64_MIN := -9223372036854775808
|
||
|
||
# master_seed 只读出口(E5 封装收口 2026-09-13):写入只经 set_master_seed(计数器全零断言
|
||
# 不可绕过);外部读 master_seed 供 存档序列化 快照契约观测。
|
||
var _master_seed: int = -1 # -1 = 未播种(取数即致命,EC7)
|
||
var master_seed: int:
|
||
get: return _master_seed
|
||
var _counters := {"board_gen": 0, "reward_roll": 0, "encounter": 0, "armor_roll": 0, "behavior_roll": 0, "variant_place": 0, "gp_drop": 0}
|
||
|
||
|
||
func _salt_for(stream: String) -> int:
|
||
match stream:
|
||
"board_gen":
|
||
return SALT_BOARD_GEN
|
||
"reward_roll":
|
||
return SALT_REWARD_ROLL
|
||
"encounter":
|
||
return SALT_ENCOUNTER
|
||
"armor_roll":
|
||
return SALT_ARMOR_ROLL
|
||
"behavior_roll":
|
||
return SALT_BEHAVIOR_ROLL
|
||
"variant_place":
|
||
return SALT_VARIANT_PLACE
|
||
"gp_drop":
|
||
return SALT_GP_DROP
|
||
push_error("[RngService] 未知名流: %s" % stream)
|
||
assert(false, "未知名流(流集合封闭,EC12)")
|
||
return 0
|
||
|
||
|
||
# 纯函数(测试/预览入口,不推计数器)
|
||
func derive_at(stream: String, scope: int) -> int:
|
||
assert(scope >= 0, "负 scope 与 scope+2^63 经输出掩码别名(禁止)")
|
||
var t: int = _master_seed + scope * GOLDEN_RATIO # wrap64 自然回绕——禁 float 中间量/禁 %/禁主动修复
|
||
var seed_val: int = _salt_for(stream) ^ t
|
||
return _mix64(seed_val)
|
||
|
||
|
||
# R3 派生终末混淆(splitmix64 finalizer 变体,2026-09-19 掷点测量修复批 B 案):裸派生种子直接作
|
||
# 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(_counters["board_gen"] == 0 and _counters["reward_roll"] == 0 and _counters["encounter"] == 0 and _counters["armor_roll"] == 0 and _counters["behavior_roll"] == 0 and _counters["variant_place"] == 0 and _counters["gp_drop"] == 0, "冒险中途重播种会击穿同冒险无碰撞前提(仅许全部流计数器全零时调用)")
|
||
_master_seed = seed_val
|
||
_counters["board_gen"] = 0
|
||
_counters["reward_roll"] = 0
|
||
_counters["variant_place"] = 0
|
||
_counters["gp_drop"] = 0
|
||
|
||
|
||
# 切片登记:冒险开始重播种入口(正式侧调用权归 冒险结构;切片主菜单 = 冒险起点)
|
||
func new_adventure(seed_val: int) -> void:
|
||
_counters["board_gen"] = 0
|
||
_counters["reward_roll"] = 0
|
||
_counters["encounter"] = 0
|
||
_counters["armor_roll"] = 0
|
||
_counters["behavior_roll"] = 0
|
||
_counters["variant_place"] = 0
|
||
_counters["gp_drop"] = 0
|
||
set_master_seed(seed_val)
|
||
|
||
|
||
# 有状态流门面:一次调用 = 一次 scope 取号(计数器读值 → 派生 → +1)
|
||
func open_stream(stream: String) -> RandomNumberGenerator:
|
||
if not STREAM_NAMES.has(stream):
|
||
push_error("[RngService] 未知名流: %s" % stream)
|
||
assert(false, "未知名流(EC12)")
|
||
if _master_seed < 0:
|
||
push_error("[RngService] 未设 master_seed 即取数(EC7——冒险结构 未接线的显式暴露)")
|
||
assert(false, "未播种流取数致命(EC7)")
|
||
var scope: int = _counters[stream]
|
||
_counters[stream] = scope + 1
|
||
var rg := RandomNumberGenerator.new()
|
||
rg.seed = derive_at(stream, scope)
|
||
return rg
|
||
|
||
|
||
# 显式 scope 取号门(behavior_roll 每敌子流专用——armor_roll 式顺序取号不适用:子流 scope =
|
||
# spawn_index × BEHAVIOR_SUBSTREAM_STRIDE + 单敌摇号序,见派生向量注记)。计数器仍单调 +1
|
||
# (总摇号次数可观测);"首个 open_stream = scope 0"语义不变(不同流不同门,互不干扰)。
|
||
func open_scope(stream: String, scope: int) -> RandomNumberGenerator:
|
||
if not STREAM_NAMES.has(stream):
|
||
push_error("[RngService] 未知名流: %s" % stream)
|
||
assert(false, "未知名流(EC12)")
|
||
assert(scope >= 0, "负 scope 与 scope+2^63 经输出掩码别名(禁止)")
|
||
if _master_seed < 0:
|
||
push_error("[RngService] 未设 master_seed 即取数(EC7)")
|
||
assert(false, "未播种流取数致命(EC7)")
|
||
_counters[stream] = _counters[stream] + 1
|
||
var rg := RandomNumberGenerator.new()
|
||
rg.seed = derive_at(stream, scope)
|
||
return rg
|
||
|
||
|
||
# 只读计数器(存档序列化 快照契约的观测面)
|
||
func counter(stream: String) -> int:
|
||
return _counters[stream]
|