Files
gplan-2/game/core/rng_service.gd
T
DKChan ea322599f7 卡②(结构小件批p1-p7+V15): 七点定点修落地+运行期违约通道负面套件+wr10 偏运缝拆验收——①p1 状态角标判据去重(main_overlay_flow._sync_status_entry 一组重复赋值语句删除,battle_live 局部量随之退役=wr1 载体);②p2 确认窗开合归模态栈单口(main 两处 _on_confirm_* 改 _close_modal;存档删除入口改 root._open_modal(root.confirm_dialog) 后再 open,遮挡记账/挂起兜底/开侧同步同口);③p3 棋盘动作与 stop 两入口显式 settlement 空防护(锁谓词异常时不崩于空引用);④p4 会话更替即清队:_begin_session 首行 _row_pacer.flush()(待播行不住局态=机制保证非时序约定);⑤p5 缝拆:RewardEconomy 删跨域直持(_fx/_ctx/set_fx)改注入 luck_bias_query: Callable,reward_roll 发射与 bias_rewards 求和收回 BattleFxHost.luck_bias_for(cell_count)(三参形 dispatch,不带 facts——禁复用 fx_emit 的第四实参会打开 when 事实条件面),组装点 set_fx 一行换赋值一行 1 换 1;类变量 15−2+2 净 15,gdstyle 无第 16 枚;⑥p6/p7 RngService 未知用途标签与未播种取数两分支加 violation_handler 记录位+release 下 return null 空引用即刻崩(fail-fast 传导,合法路径不可达)——debug 下 assert 先中断函数本体两态不可判别,故记录位是唯一可观测面;⑦board_generator/reward_economy 各加 fatal_handler 注入位并由组装点接 orchestrator._fatal(release 剥 assert 后仍经 battle_result 封印真中止)。新件 tests/unit/core/run_time_fatal_test.gd(manifest 登记 expect=1 negative=true):wr5 协作件 _fatal 经组装点传导 p5 封印+缺省形态不外溢/wr6 未知标签通道/wr7 未播种通道(各用新建 RngService 实例,播种断言 _counter==0)。wr10 三条入 variant_placement_test=判定面夹具白盒装载(现网数据无 bias_rewards 行,沿 tree_rules_v11_test 先例)+接线同源+权重生效 77+4*2。验收:variant_placement 49 checks 全绿(wr10 ①②③ PASS:空订阅 0/命中 1 行/装载后 2;查询口有效且与宿主同源;偏运池实测 85);run_time_fatal_test 退出码 1 三点绿;pacing_queue 绿(wr4 前置夹具真入队 2 行→更替后清零);ui_click_probe 问题 0(wr2 遮挡账开/关两侧断言);round_flow_skill_pool 130 checks 绿;wr11 dump_battle 六组(L{1,2}×S{42,779401,779501})与改前 before_* 逐字节 cmp 全等;gdstyle 基线外新违规 0、恶化 1(level_map_screen 类变量 22>21=iter38 存量非本批触碰面)、基线文件零改动〔结构小件批 V15〕
2026-10-27 18:39:53 +08:00

160 lines
9.1 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 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)