feat(rng): 统一随机流批落地(ADR-0007 裁丙)——rng_service 七流并一+全事件日志:单盐单全局计数器+用途标签 open_stream(purpose) 不参与派生+derive_at(scope) 单参+事件日志会话制 rng_log_<seed>.jsonl;七消费点迁移+behavior_roll 子流派生向量退役(单敌轨迹独立复放面随退)+armor_roll_stream 降格纯标签;统一向量 8 常量重登记(scope0-3 与旧 board_gen 逐位连续);测试重锚(nat20/极性路由/L2max 改行为面扫描+ui_input_probe 移位扩边前置);冒烟 557/0+四探针全绿+MC20 局+日志落盘;数据配置 v0.5/registry 两实体/冒险结构 v0.10/节点图生成 v0.4 前置解除/ADR-0007 裁决落档+落地记录/CLAUDE.md 红线措辞/06 架构批条目解决移除

This commit is contained in:
DKChan committed 2026-09-27 20:01:29 +08:00
1 parent afcc018f34
commit 24123616d8
15 files changed
+226 -217

No files matched your search

+4 -5
View File
@@ -522,10 +522,10 @@ func _grant_clear_identity(snap: Dictionary, ri: int, suffix: String) -> bool:
# ---- 开轮摇号(敌人技能池批——共识点 B:技能选择前置到轮次开始,数值计算留结算段)----
# 每实例:保底判定(连续执行普攻 ≥ M → 必出特殊技、特殊技内按权重、不受冷却限制)→ 排除冷却中技能
# (中签率 = 权重 ÷ 可摇权重合计——份额归一)→ 按行序逐敌从 behavior_roll 独立子流摇定本拍动作。
# (中签率 = 权重 ÷ 可摇权重合计——份额归一)→ 按行序逐敌从统一流顺序取号摇定本拍动作。
# 随机消费只在换算层(本方法 + SkillLottery);单位容器收确定值(零随机纪律——armor_roll 先例同构)。
# 子流 scope = spawn_index × BEHAVIOR_SUBSTREAM_STRIDE + 单敌摇号序(registry 派生向量)——
# 单敌出招轨迹可独立复放、敌间互不穿插。摇定后、兑现前死亡 → 本拍技能作废(死亡即不动手)。
# 并流批(ADR-0007 裁丙 2026-09-27):behavior_roll 子流派生向量(spawn_index×STRIDE+序)退役——
# 「单敌轨迹独立复放」前提随之移除(位置交错,单点定位由事件日志承载)。摇定后、兑现前死亡 → 本拍技能作废。
func _roll_round_actions() -> void:
var pity_m: int = int(db.skill.pity_threshold)
for u in _enemy_system.query_alive("enemy"):
@@ -534,8 +534,7 @@ func _roll_round_actions() -> void:
var specials: Array = _enemy_system.special_skills(u)
if not specials.is_empty():
entries = specials # 保底命中——普攻恒可摇 → 非保底路径池恒非空,此为唯一替代集合
var scope: int = int(u.get("handle_id", 0)) * RngService.BEHAVIOR_SUBSTREAM_STRIDE + int(u.get("roll_seq", 0))
var stream: RandomNumberGenerator = rng.open_scope("behavior_roll", scope)
var stream: RandomNumberGenerator = rng.open_stream("behavior_roll")
_enemy_system.set_pending_action(u, SkillLottery.draw(entries, stream))
_enemy_system.tick_cooldowns() # 递减在摇号读值之后——"施放后连续 N 拍不可摇中"的等价实现(单位容器 注记)
+71 -80
View File
@@ -1,30 +1,28 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
# Date: 2026-09-10
# RngService autoload(数据配置)——封闭命名流单点(Rule 5)。玩法代码禁止 randomize()/自建 RandomNumberGenerator。
# RngService autoload(数据配置)——统一随机流单点(Rule 5;ADR-0007 裁丙 2026-09-27:七流并一+全事件日志)。
# 玩法代码禁止 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(地图战斗资源 预推进术语的登记口径)。
# 统一流口径:唯一主种子 + 唯一派生(单盐)+ 唯一全局取数计数器;open_stream 参数降格为「用途标签」——
# 不参与派生,只进事件日志(并流前七条流名全部保留为合法标签)。号位语义(ADR-0007 后果,用户裁决承接):
# 全局 scope 只回答「全局第 N 次取数」,不再回答「第 N 次某类事件」——路线依赖为已知代价,单点定位能力由事件日志承载。
# 派生公式(R3 2026-09-19 + 统一流 2026-09-27):derive = mix64(SINGLE_SALT XOR wrap64(master + scope×GOLDEN))——
# splitmix64 finalizer 变体终末混淆,见 _mix64 注记;并流前分盐黄金向量随批作废,新向量按单盐重登记(scope 0-3 与
# 旧 board_gen 向量逐位连续——单盐沿用原 board_gen 盐)。
# 取号语义:全新冒险首个 open_stream = 全局 scope 0;事件日志逐次落盘 user://logs/rng_log_<master_seed>.jsonl。
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 条目随批同步)
# 用途标签封闭词表(并流前七流名全保留;新增用途走扩签通道 = 改代码 + 评审——清单封闭语义由「流」改「标签」)
const PURPOSE_TAGS := ["board_gen", "reward_roll", "encounter", "armor_roll", "behavior_roll", "variant_place", "gp_drop"]
# 分段表达式书写(数据配置 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 同批登记)
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 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
@@ -33,36 +31,18 @@ const INT64_MIN := -9223372036854775808
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}
var _counter: int = 0 # 全局取数计数器(唯一账本——快照契约 = 主种子 + 本计数器)
# 事件日志(丙案新增):每冒险一会话,逐次取数落一行(scope/用途标签/派生种子)——不致命、失败静默降级
var _log_path := ""
var _log_fa: FileAccess = null
var _log_warned := false
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:
# 纯函数(测试/预览/日志回放入口,不推计数器)——单盐单参数(并流前 (流, scope) 双参退役)
func derive_at(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)
return _mix64(SINGLE_SALT ^ t)
# R3 派生终末混淆(splitmix64 finalizer 变体,2026-09-19 掷点测量修复批 B 案):裸派生种子直接作
@@ -85,58 +65,69 @@ func set_master_seed(seed_val: int) -> void:
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, "冒险中途重播种会击穿同冒险无碰撞前提(仅许全部流计数器全零时调用)")
assert(_counter == 0, "冒险中途重播种会击穿同冒险无碰撞前提(仅许全局计数器归零时调用)")
_master_seed = seed_val
_counters["board_gen"] = 0
_counters["reward_roll"] = 0
_counters["variant_place"] = 0
_counters["gp_drop"] = 0
_counter = 0
# 切片登记:冒险开始重播种入口(正式侧调用权归 冒险结构;切片主菜单 = 冒险起点)
# 切片登记:冒险开始重播种入口(正式侧调用权归 冒险结构;切片主菜单 = 冒险起点)——
# 并流批(ADR-0007 裁丙):重播种同时开本次冒险的事件日志会话。
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
_counter = 0
set_master_seed(seed_val)
_open_log_session(_master_seed)
# 有状态流门面:一次调用 = 一次 scope 取号(计数器读值 → 派生 → +1)
func open_stream(stream: String) -> RandomNumberGenerator:
if not STREAM_NAMES.has(stream):
push_error("[RngService] 未知名流: %s" % stream)
assert(false, "未知名流(EC12)")
# 取号门面:一次调用 = 一次全局 scope 取号(计数器读值 → 派生 → +1 → 落日志)。
# purpose 不参与派生——只作事件日志的用途标签(七流名保留为合法标签词表)。
func open_stream(purpose: String) -> RandomNumberGenerator:
if not PURPOSE_TAGS.has(purpose):
push_error("[RngService] 未知用途标签: %s" % purpose)
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
assert(false, "未播种取数致命(EC7)")
var scope: int = _counter
_counter = scope + 1
var rg := RandomNumberGenerator.new()
rg.seed = derive_at(stream, scope)
var dseed: int = derive_at(scope)
rg.seed = dseed
_log_draw(purpose, scope, dseed)
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() -> int:
return _counter
# 只读计数器(存档序列化 快照契约的观测面)
func counter(stream: String) -> int:
return _counters[stream]
# ---- 事件日志(丙案:全事件落盘——号位失去事件语义后的单点定位承载面)----
# 会话入口:只由 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)
+2 -2
View File
@@ -85,7 +85,7 @@ func spawn(level_index: int, node_depth: int, hero_hp: int, unit_count_override:
"atk": maxi(0, int(round(float(eff_row_atk) * atk_mult))),
"pool": pool_snap, # 实例池(模板快照 + 冷却状态——独立摇号互不干涉)
"pending_skill": "", # 本拍摇定动作(开轮摇号写入;兑现前死亡 = 作废不 commit)
"roll_seq": 0, # 单敌摇号序(behavior_roll 子流 scope 派生向量——registry 登记)
"roll_seq": 0, # 单敌摇号拍序(诊断标签——并流后不参与派生,ADR-0007 裁丙 2026-09-27)
"basic_streak": 0, # 保底计数:连续执行普攻的拍数(执行上报驱动)
"armor_type": String(row.get("armor_type", "light")), # 护甲类型快照(克制矩阵消费面——战斗结算·战斗段;2026-09-15 命名统一批:light/ward/heavy)
"armor_class": int(row.get("armor_class", 0)), # 护甲等级快照(掷点判定阈值基数——战斗结算·战斗段;2026-09-16 护甲等级掷点批)
@@ -138,7 +138,7 @@ func special_skills(u: Dictionary) -> Array:
return out
# 摇定结果写入(战斗结算换算层调用——确定值;roll_seq 递增 = 子流 scope 派生向量的取号序)
# 摇定结果写入(战斗结算换算层调用——确定值;roll_seq 递增 = 单敌第几拍诊断标签,不参与派生)
func set_pending_action(u: Dictionary, skill_id: String) -> void:
if u.is_empty() or _despawned or u.get("is_hero", false):
_diag("set_pending_action 失效句柄")