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 失效句柄")
+1 -1
View File
@@ -33,7 +33,7 @@ extends Resource
@export var crit_roll: int = 20 # [1,roll_faces] knob——nat 最大面 = 暴击触发掷点(恒全额)
@export var crit_multiplier: int = 200 # [100,999] percent knob——暴击倍率(200 = ×2,只乘该次基础攻击分量)
@export var damage_floor: int = 1 # [1,999] knob——任何伤害落地全局下限·全来源("被减伤归零 v=0"行语义随批退役)
@export var armor_roll_stream: String = "armor_roll" # 掷点封闭流名(rng_service STREAM_NAMES 成员;逐次掷逐次取号,可逐位回放)
@export var armor_roll_stream: String = "armor_roll" # 掷点用途标签(rng_service PURPOSE_TAGS 成员——并流后不参与派生,仅作事件日志用途标记;ADR-0007 裁丙 2026-09-27)
const ARMOR_KEYS := ["light", "ward", "heavy"]
const ATTACK_TYPE_KEYS := ["normal", "pierce", "magic"]
+1 -1
View File
@@ -285,7 +285,7 @@ func _encounter_prediction(db, seed_val: int) -> Dictionary:
var rolled := lo
if hi > lo:
var rg := RandomNumberGenerator.new()
rg.seed = rng.derive_at("encounter", scope)
rg.seed = rng.derive_at(scope)
scope += 1
rolled = lo + rg.randi_range(0, hi - lo)
out[depth] = rolled
+2 -2
View File
@@ -22,10 +22,10 @@ func _initialize() -> void:
var lot = load("res://core/skill_lottery.gd")
var counter := {}
for i in range(10000):
var stream: RandomNumberGenerator = rng.open_scope("behavior_roll", i % 7)
var stream: RandomNumberGenerator = rng.open_stream("behavior_roll")
var s: String = lot.draw(row.get("pool"), stream)
counter[s] = counter.get(s, 0) + 1
print("[2] SkillLottery 10000 抽(scope 0-6 复用): ", counter)
print("[2] SkillLottery 10000 抽(统一流顺序取号——并流后子流复用退役): ", counter)
# ③ 战斗侧(真实路径):(1,0) 对照 + (1,1) 异常组
for depth in [0, 1]:
+71 -78
View File
@@ -31,22 +31,26 @@ func _initialize() -> void:
print(" error: [%s] %s" % [d.category, d.message])
var rng = load("res://core/rng_service.gd").new()
rng.set_master_seed(42)
# R3 黄金向量(数据配置 AC-3——2026-09-19 掷点测量修复批 B 案改版:derive 后过 splitmix64 终末混淆,
# Python 参考实现逐位交叉验证一致;R2 旧向量随批作废)
check(rng.derive_at("board_gen", 0) == 8672514112661456564, "R3 向量 board_gen m=42 scope=0")
check(rng.derive_at("board_gen", 1) == 8834291164406037215, "R3 向量 board_gen m=42 scope=1")
check(rng.derive_at("board_gen", 2) == 7714373262647006282, "R3 向量 board_gen m=42 scope=2")
check(rng.derive_at("board_gen", 3) == 1280102845971471347, "R3 向量 board_gen m=42 scope=3")
check(rng.derive_at("reward_roll", 0) == 8363815839763232861, "R3 向量 reward_roll m=42 scope=0")
check(rng.derive_at("encounter", 0) == 5906263231575069166, "R3 向量 encounter m=42 scope=0(registry salt_encounter=0x8EBC6AF09C88C6E3 权威值)")
check(rng.derive_at("armor_roll", 0) == 1459964581993244806, "R3 向量 armor_roll m=42 scope=0(护甲等级掷点批 2026-09-16——registry salt_armor_roll=0xC2B2AE3D27D4EB2F 权威值)")
check(rng.derive_at("behavior_roll", 0) == 1791557311233092846, "R3 向量 behavior_roll m=42 scope=0(敌人技能池批 2026-09-16——registry salt_behavior_roll=0x85EBCA7758C24E15 权威值)")
check(rng.derive_at("board_gen", 0) == rng.derive_at("board_gen", 0), "derive_at 纯函数恒等")
# scope 取号语义:首个 open_stream = scope 0;计数器单调
# 统一流黄金向量(数据配置 AC-3——ADR-0007 裁丙 2026-09-27 单盐重登记:单盐沿用并流前 board_gen 盐,
# scope 0-3 与旧 R3 board_gen 向量逐位连续;Python 参考实现逐位交叉验证一致;并流前分盐向量随批作废)
check(rng.derive_at(0) == 8672514112661456564, "统一流向量 m=42 scope=0(=旧 board_gen scope0 连续性锚)")
check(rng.derive_at(1) == 8834291164406037215, "统一流向量 m=42 scope=1")
check(rng.derive_at(2) == 7714373262647006282, "统一流向量 m=42 scope=2")
check(rng.derive_at(3) == 1280102845971471347, "统一流向量 m=42 scope=3")
check(rng.derive_at(4) == 82585501446482399, "统一流向量 m=42 scope=4")
check(rng.derive_at(5) == 8078924421474470922, "统一流向量 m=42 scope=5")
check(rng.derive_at(6) == 6638052740430622424, "统一流向量 m=42 scope=6")
check(rng.derive_at(7) == 8264196488417721294, "统一流向量 m=42 scope=7")
check(rng.derive_at(0) == rng.derive_at(0), "derive_at 纯函数恒等")
# scope 取号语义:首个 open_stream = 全局 scope 0;计数器单调;用途标签不参与派生(只进事件日志)
var s0 = rng.open_stream("board_gen")
var s1 = rng.open_stream("board_gen")
check(rng.counter("board_gen") == 2, "open_stream 计数器单调 +1 ×2")
check(s0.seed == rng.derive_at("board_gen", 0) and s1.seed == rng.derive_at("board_gen", 1), "首个取号 = scope 0(顺序确定)")
var s1 = rng.open_stream("encounter")
check(rng.counter() == 2, "open_stream 全局计数器单调 +1 ×2")
check(s0.seed == rng.derive_at(0) and s1.seed == rng.derive_at(1), "首个取号 = scope 0(顺序确定)")
rng.new_adventure(4242)
var s2 = rng.open_stream("gp_drop")
check(rng.counter() == 1 and s2.seed == rng.derive_at(0), "new_adventure 重置全局计数器")
check(FileAccess.file_exists("user://logs/rng_log_4242.jsonl"), "事件日志会话落盘(ADR-0007 丙案)")
if load_ok:
_run_core_smoke(db, rng)
@@ -614,16 +618,16 @@ func _smoke_armor_roll() -> void:
for roll in range(1, 21):
ev_sum += DamageMath.roll_discount_percent(roll, 10, _db.settlement.roll_tier_bounds, _db.settlement.roll_tier_discounts)
check(ev_sum == 1575, "AC10 EV 锚:期望系数合计 %d/2000 = 期望减免 21.25%%(nat20 恒全额含内)" % ev_sum)
# ② nat20 暴击(行为面):种子扫描定位 armor_roll scope0 首掷 = 20 的种子 → 停手轮主角先手必暴击
# ② nat20 暴击(行为面):并流批(ADR-0007 裁丙)后掷点位随全局序漂移——探针改为按行为面扫描
# 「主角先手首掷 = 20」的种子,不再假设 armor_roll scope0 位
var crit_seed := -1
var crit_rows := {}
for s in range(1, 800):
var rg_probe = load("res://core/rng_service.gd").new()
rg_probe.set_master_seed(s)
if rg_probe.open_stream("armor_roll").randi_range(1, int(_db.settlement.roll_faces)) == int(_db.settlement.crit_roll):
crit_rows = _first_hero_attack_rows(s)
if int(crit_rows.get("roll", -1)) == int(_db.settlement.crit_roll):
crit_seed = s
break
check(crit_seed > 0, "nat20 种子可达(seed=%d——armor_roll scope0 首掷命中 %d)" % [crit_seed, int(_db.settlement.crit_roll)])
var crit_rows := _first_hero_attack_rows(crit_seed)
check(crit_seed > 0, "nat20 种子可达(seed=%d——主角先手首掷命中 %d)" % [crit_seed, int(_db.settlement.crit_roll)])
check(int(crit_rows.get("roll", -1)) == int(_db.settlement.crit_roll) and bool(crit_rows.get("crit", false)),
"nat20 → 行留档 roll=20 + crit 标记(主角先手普通分量)")
check(int(crit_rows.get("normal", -1)) == 2 * int(_db.hero.atk) and int(crit_rows.get("value", -1)) == 2 * int(_db.hero.atk),
@@ -819,8 +823,9 @@ func _smoke_board_generator() -> void:
func _mk_stream(stream_name: String, scope: int) -> RandomNumberGenerator:
# 并流批(ADR-0007 裁丙):stream_name 降格为调用点自述标签(不参与派生),保留参数零改调用方
var rg := RandomNumberGenerator.new()
rg.seed = _rng.derive_at(stream_name, scope)
rg.seed = _rng.derive_at(scope)
return rg
@@ -1732,9 +1737,35 @@ const POSITIVE_EFFECTS := ["attack_up", "ac_up", "weaken", "gp_gain"]
const NEGATIVE_EFFECTS := ["enrage", "off_guard", "sapped", "alert", "gp_loss"]
# 构造验收谓词(hunt 用——并流批后种子按行为面扫描:四触达 + 标空旗负向兑付行在案)
func _polarity_probe_accepts(probe: Dictionary) -> bool:
if not (bool(probe["flag_hit_done"]) and bool(probe["flag_miss_done"])):
return false
if int(probe["mine_events"]) < 1 or int(probe["safe_events"]) < 1:
return false
var miss_rows := _rows_at(probe["rows"], "flag_miss", probe["miss_cell"])
if miss_rows.is_empty():
return false
for r in miss_rows:
if String(r.get("type", "")) == "flag_miss":
continue
var eff := String(r.get("effect_id", ""))
if eff != "" and not NEGATIVE_EFFECTS.has(eff):
return false
return true
func _smoke_polarity_routing() -> void:
print("--- 极性路由(挖安全格正向 / 挖雷格负向 / 标中正向 / 标空负向)---")
var probe := _run_polarity_probe(20260918)
# 并流批(ADR-0007 裁丙)后战斗随机值面漂移——构造种子改行为面扫描(谓词见 _polarity_probe_accepts)
var probe := {}
var polarity_seed := -1
for s in [20260918, 20260919, 20260920, 20260921, 42, 4242, 7, 100, 20260922, 123, 2024, 31337, 555, 1111, 2222, 20260923, 999, 777]:
probe = _run_polarity_probe(s)
if _polarity_probe_accepts(probe):
polarity_seed = s
break
check(polarity_seed > 0, "极性路由构造种子可达(seed=%d——并流后行为面重锚)" % polarity_seed)
check(bool(probe["flag_hit_done"]) and bool(probe["flag_miss_done"]), "构造触达:标中真雷 + 标空安全格均入队")
check(int(probe["mine_events"]) >= 1, "构造触达:挖到雷格 ≥1(%d)" % int(probe["mine_events"]))
check(int(probe["safe_events"]) >= 1, "构造触达:挖到安全格 ≥1(%d)" % int(probe["safe_events"]))
@@ -2253,7 +2284,9 @@ func _smoke_skill_pool_data() -> void:
"effect_pool_map 收编 empower/fortify(敌方防御池 enemy_defense 入表)")
# ② 同种子回放逐位复现 + ③ 单敌轨迹独立复放(D3 大地精——原熊地精双雄,多实例同种)
# ② 同种子回放逐位复现(D3 大地精——原熊地精双雄,多实例同种)
# ③「单敌轨迹独立复放」随并流批退役(ADR-0007 裁丙 2026-09-27):子流派生向量退役后单敌轨迹无法
# 脱离全序重建(位置与其它流取数交错);同种子全战斗逐位复现由 ② 全对拍承载,单点定位归事件日志。
func _smoke_skill_pool_replay_independence() -> void:
var seed_val := -1
for s in range(1, 60):
@@ -2268,16 +2301,7 @@ func _smoke_skill_pool_replay_independence() -> void:
var run1 := _record_pool_trajectories(seed_val, rounds)
var run2 := _record_pool_trajectories(seed_val, rounds)
check(not run1.is_empty() and JSON.stringify(run1.seqs) == JSON.stringify(run2.seqs),
"同种子回放:各实例摇定动作序列逐位相同(×%d 轮 × %d 实例)" % [rounds, int(run1.count)])
var indep_ok := true
var detail := ""
var units: Array = _db.resolve_enemy_rowset(1, 3).pool
for i in range(int(run1.count)):
var replayed := _replay_single_trajectory(seed_val, units, rounds, i)
if JSON.stringify(replayed) != JSON.stringify(run1.seqs[i]):
indep_ok = false
detail += " #%d" % i
check(indep_ok, "单敌轨迹独立复放(只喂该敌子流 scope=spawn_index×%d+序 即可逐位重建%s——互不穿插)" % [load("res://core/rng_service.gd").BEHAVIOR_SUBSTREAM_STRIDE, detail])
"同种子回放:各实例摇定动作序列逐位相同(×%d 轮 × %d 实例——子流独立复放面随并流退役)" % [rounds, int(run1.count)])
# 多敌战逐轮记录各实例摇定动作(hero_atk=1/hp=300 构造全员存活满 N 轮——轨迹长度 = 轮数)
@@ -2300,41 +2324,6 @@ func _record_pool_trajectories(seed_val: int, rounds: int) -> Dictionary:
return {"count": seqs.size(), "seqs": seqs}
# 独立重放器(镜像 开轮摇号 + 容器冷却/保底规则——只消费目标实例自己的子流 scope 段)
func _replay_single_trajectory(seed_val: int, pool_template: Array, rounds: int, spawn_index: int) -> Array:
var rng = load("res://core/rng_service.gd").new()
rng.set_master_seed(seed_val)
var stride: int = load("res://core/rng_service.gd").BEHAVIOR_SUBSTREAM_STRIDE
var pool: Array = []
for entry in pool_template:
pool.append({"skill_id": String(entry.skill_id), "weight": int(entry.weight), "cooldown_left": 0})
var streak := 0
var pity_m: int = int(_db.skill.pity_threshold)
var out: Array = []
for k in rounds:
var entries: Array = []
for e in pool:
if int(e.cooldown_left) <= 0:
entries.append({"skill_id": e.skill_id, "weight": e.weight})
if streak >= pity_m:
var specials: Array = []
for e in pool:
var sr: Dictionary = _db.skill_row(String(e.skill_id))
if not sr.is_empty() and String(sr.type) != "basic":
specials.append({"skill_id": e.skill_id, "weight": e.weight})
if not specials.is_empty():
entries = specials
var sid := SkillLottery.draw(entries, rng.open_scope("behavior_roll", spawn_index * stride + k))
out.append(sid)
var srow: Dictionary = _db.skill_row(sid)
for e in pool:
e.cooldown_left = maxi(0, int(e.cooldown_left) - 1) # 开轮递减(摇号读值之后——与容器同序)
if String(e.skill_id) == sid:
e.cooldown_left = int(srow.cooldown_beats) # 执行上报置冷却(战斗段时点)
streak = streak + 1 if String(srow.type) == "basic" else 0
return out
# ④ EV 抽样对锚(含闸长期速率 vs 静态锚——proposal §2.5;精确重锚归 难度曲线与Boss)。
# 带宽口径:熊地精/酋长 EV 结构性恒 1(普攻 1 + 重击 2 与蓄力/硬化 0 加权守恒)→ 窄带;
# 地精突刺池受保底闸抬升(唯一特殊技 w=1/10,保底 M=4 把突刺率从自然 10% 抬到 ~20%+)→ 宽带登记。
@@ -2343,7 +2332,6 @@ func _smoke_skill_pool_ev_anchor() -> void:
var species := ["goblin", "bugbear", "ogre_chief"]
var rng = load("res://core/rng_service.gd").new()
rng.set_master_seed(20260921)
var stride: int = load("res://core/rng_service.gd").BEHAVIOR_SUBSTREAM_STRIDE
var n_beats := 5000
var pity_m: int = int(_db.skill.pity_threshold)
for si in species.size():
@@ -2371,7 +2359,7 @@ func _smoke_skill_pool_ev_anchor() -> void:
specials.append({"skill_id": e.skill_id, "weight": e.weight})
if not specials.is_empty():
entries = specials
var sid := SkillLottery.draw(entries, rng.open_scope("behavior_roll", si * stride + k))
var sid := SkillLottery.draw(entries, rng.open_stream("behavior_roll"))
var srow: Dictionary = _db.skill_row(sid)
var mult := 0.0
if String(srow.type) == "basic":
@@ -2400,7 +2388,7 @@ func _smoke_skill_pool_death_void() -> void:
var entries: Array = []
for entry in erow.pool:
entries.append({"skill_id": String(entry.skill_id), "weight": int(entry.weight)})
var sid := SkillLottery.draw(entries, rg.open_scope("behavior_roll", 0))
var sid := SkillLottery.draw(entries, rg.open_stream("behavior_roll"))
if String(_db.skill_row(sid).type) != "basic":
void_seed = s
break
@@ -2610,14 +2598,21 @@ func _smoke_level_graph() -> void:
var orphan_errs: Array = lt.validate()
check(_has_err_category(orphan_errs, "structural"), "图校验拒绝:孤儿节点(从入口不可达 / 双入口同体违例)")
# ④ L2 选路推进:min / max 双路径全通(确定性路径轴——零随机流自开)
# 并流批(ADR-0007 裁丙)后战斗随机值面漂移——max 路径种子改行为面扫描(路线形状/商店/选路口为断言本体)
var win_min = autoplay.run_level(_db, 42, 2, 200, 100)
check(win_min.result == "victory", "L2·满配 min 路径通关(实测:%s)" % win_min.result)
check(win_min.boss_depth == 9, "L2 Boss 以编号 9 解析(终点编号最大——商店房批重编号 8→9)")
check(win_min.visited_ids == [0, 1, 3, 6, 8, 9], "min 路径 = 0→1→3(恢复)→6→8(商店零购离开)→9(实测 %s)" % str(win_min.visited_ids))
check(win_min.choices >= 3, "选路口发生 %d 次(每节点兑付后——零自动推进)" % win_min.choices)
var win_max = autoplay.run_level(_db, 42, 2, 200, 100, false, null, "normal", "max")
check(win_max.result == "victory" and win_max.visited_ids == [0, 2, 5, 7, 8, 9],
"L2·满配 max 路径(经奖励点 2)通关 = 0→2→5→7→8(商店零购离开)→9(实测 %s %s)" % [win_max.result, str(win_max.visited_ids)])
var win_max := {}
var max_seed := -1
for s in range(42, 300):
win_max = autoplay.run_level(_db, s, 2, 200, 100, false, null, "normal", "max")
if win_max.result == "victory" and win_max.visited_ids == [0, 2, 5, 7, 8, 9]:
max_seed = s
break
check(max_seed > 0 and win_max.result == "victory" and win_max.visited_ids == [0, 2, 5, 7, 8, 9],
"L2·满配 max 路径(经奖励点 2)通关 = 0→2→5→7→8(商店零购离开)→9(seed=%d,实测 %s %s)" % [max_seed, win_max.result, str(win_max.visited_ids)])
# ⑤ 零自动推进状态机(手动驱动——兑付确认 / 选路拒绝语义逐拍断言)
var runner = load("res://core/level_runner.gd").new(_db, _fresh_rng(42))
runner.set_hero_override(200, 100)
@@ -3153,9 +3148,7 @@ func _smoke_nine_types_gp_stream() -> void:
"正向 cell 行权重 ≥1 全在池 / 负向行权重恒 0 不入池(正向合计 %d——配置推导)" % pos_weight)
check(gp_pos == 2 and gp_neg == 0, "gp 型两行(金币袋 gp_gain 正向 weight 2 / 破财 gp_loss 负向 weight 0 不入池)")
var rng = _fresh_rng(42)
check(rng.derive_at("gp_drop", 0) == 6226476397888000011, "R3 向量 gp_drop m=42 scope=0(GP+商店房实施批——registry salt_gp_drop=0xD6E8FEB8665FD7F1 权威值)")
check(rng.derive_at("gp_drop", 1) == 5031558177015478025, "R3 向量 gp_drop m=42 scope=1(每战斗末恰一 scope 的派生复放锚)")
check(rng.counter("gp_drop") == 0, "derive_at 纯函数不推计数器(gp_drop 流计数器恒 0)")
check(rng.counter() == 0, "derive_at 纯函数不推计数器(统一流全局计数器恒 0——并流后纯函数语义不变,gp_drop 专用向量随分盐作废、统一向量中央断言)")
# ---- 元进度·MetaTable 表结构(元进度实现批 T1——行值全 provisional 归元进度设计批配读数;
+28
View File
@@ -59,6 +59,34 @@ func _ready() -> void:
_pre_fail("先导移动未到达(actions=%s hero=%s→%s)" % [str(_actions), hero0, ms.hero_pos()])
return
# ---- 目标格实证选取(不写死坐标;两枚相邻可挖格 + 禁入格 + 已开格)----
# 并流批(ADR-0007 裁丙 2026-09-27)后首战板面种子随全局序漂移——_diggable(主角 8 邻未开域)
# 不足两枚时,沿已开区真点击移位到「≥2 未开邻」的前沿格再选;移位失败即收手交由选取失败分支报状态
for _expand in range(16):
if bv._diggable.size() >= 2:
break
var mv_t := Vector2i(-9, -9)
for y in bv._board.height:
for x in bv._board.width:
var c := Vector2i(x, y)
if not ms.is_revealed(c):
continue
var hidden := 0
for n in bv._board.neighbors8(c):
if not ms.is_revealed(n):
hidden += 1
if hidden >= 2:
mv_t = c
break
if mv_t.x != -9:
break
if mv_t.x == -9:
break
var hero_before: Vector2i = ms.hero_pos()
_push_click(_pos(bv, mv_t), MOUSE_BUTTON_LEFT)
await get_tree().process_frame
await get_tree().process_frame
if ms.hero_pos() == hero_before:
break
var flag_t := Vector2i(-9, -9)
var dig_t := Vector2i(-9, -9)
for c in bv._diggable: