diff --git a/CLAUDE.md b/CLAUDE.md index 666475e0..e169de63 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -69,7 +69,7 @@ - **worktree 铁律**:主工作区禁止直接改动与提交(细则见上节;并行会话各占 worktree 互不踩踏) - **数值单一事实源**:玩法数值只存 `data/tables/`(.tres),经 `ConfigDB` autoload 类型化单点读取;禁止数值字面量与自建 `load()` 表路径 -- **RNG 单点**:禁止 `randomize()` / 自建 RandomNumberGenerator——随机只经 `RngService` 封闭命名流 +- **RNG 单点**:禁止 `randomize()` / 自建 RandomNumberGenerator——随机只经 `RngService` 统一流(单盐单计数器+用途标签+全事件日志,ADR-0007 裁丙 2026-09-27) - **Core 层零 UI 依赖**:完整战斗必须可 `--headless --script` 跑通 - **fail-fast**:数据校验错误一律致命中止,无软回退 - **spec-AC 一致性**:规约与验收标准不得在同一输入上给出不同答案(所有 GDD 同尺度) diff --git a/design/dev/architecture/adr-0007-rng-single-stream.md b/design/dev/architecture/adr-0007-rng-single-stream.md index 2dc2ad81..efc7e4b6 100644 --- a/design/dev/architecture/adr-0007-rng-single-stream.md +++ b/design/dev/architecture/adr-0007-rng-single-stream.md @@ -2,7 +2,7 @@ ## Status -评估书——**待用户裁决**(本 ADR 是裁决闸,裁决前不动任何代码)。 +**已裁决:方案丙(并流 + 全事件日志)——2026-09-27 用户裁决,随批落地**。推荐案甲未被采纳,评估书正文保留原样作决策记录;丙的实施 = 统一随机流批(rng_service.gd 重写 + 七消费点迁移 + 单盐黄金向量重登记 + 事件日志会话制),验证与工单清单见文末「落地记录」。 背景:2026-09-27 用户在[关卡节点图生成](../../gdd/systems/冒险循环/关卡节点图生成.md)拍板点②两轮复裁终选「随机数统一用一个流」(全面执行),并拍板执行路径 = **先出本评估书再动手**、节点图特性实施排队其后。本评估书按用户要求如实给出:现行机制事实、统一流的真实改动面、三个备选与推荐。裁决本 ADR 后按裁决执行。 @@ -71,3 +71,10 @@ - 裁决甲:节点图生成实施批按第八栏增流推进(改代码+金向量+冒烟断言三件套),本评估书归档为架构记录。 - 裁决乙:先开 rng 架构批(迁移工单如上),全量回归后节点图生成实施批接续;历史 MC 基线全部作废重测归曲线调参批。 - 裁决丙:同乙,另加日志面设计与写盘性能验收。 + +## 落地记录(统一随机流批 2026-09-27) + +- 代码:rng_service.gd 重写(单盐 SINGLE_SALT=原 board_gen 盐 / 全局计数器 / 用途标签 open_stream(purpose) / derive_at(scope) 单参 / open_scope 与 BEHAVIOR_SUBSTREAM_STRIDE 退役 / 事件日志会话制 user://logs/rng_log_<主种子>.jsonl);消费点七处迁移(board_generator/reward_economy/battle_orchestrator×4/damage_probe/skill_rhythm_probe/smoke_all 向量与计数断言);behavior_roll 子流派生向量退役(逐敌顺序取号替代,单敌轨迹独立复放面随之退役——冒烟 ③ 测试同批退役,同种子全战斗对拍由 ② 承载)。 +- 向量:统一流 8 常量重登记(scope 0-3 与旧 board_gen 向量逐位连续——单盐沿用其盐);registry stream_seed_derivation 全量改版。 +- 测试:冒烟 557/0 三闸全过(nat20/极性路由/L2 max 三处种子硬编码测试改行为面扫描——并流后随机值面漂移的测试面重锚);四场景探针全绿(ui_input_probe 加「移位扩边」前置——板面种子漂移下前沿不足两格时沿已开区点击移位);MC 20 局逐局独立跑通,事件日志逐冒险落盘实证。 +- 行为面基线:并流后全部历史 MC/verifier 基线作废,重测归曲线调参批(测量对账条款既有口径)。 diff --git a/design/gdd/06-待办清单.md b/design/gdd/06-待办清单.md index 9551cd34..27464e57 100644 --- a/design/gdd/06-待办清单.md +++ b/design/gdd/06-待办清单.md @@ -219,7 +219,7 @@ **归属**:[关卡节点图生成](systems/冒险循环/关卡节点图生成.md)(设计 owner;行集内容责任方仍为[冒险结构](systems/冒险循环/冒险结构.md),行进与图合法性校验归[关卡节点地图](systems/冒险循环/关卡节点地图.md)) -**状态**:已立项·设计草案 v0.3(拍板点全落文;实施前置=ADR-0007 裁决落地) +**状态**:已立项·设计草案 v0.4(拍板点全落文;实施前置已解除——统一随机流批 ADR-0007 裁丙已落地,待实施批) ### 难度曲线与Boss @@ -379,15 +379,6 @@ NodeOverlay 现行实态为模态覆盖层:恢复点全量服务(回复读 ### 数据配置 -#### 全游戏随机流统一架构批(七流并一评估,ADR-0007) - -**现状**:2026-09-27 用户两轮复裁终选「随机数统一用一个流」,先出评估书再动手。评估书已交付:[ADR-0007](../dev/architecture/adr-0007-rng-single-stream.md)——核心发现:字面目标「单一随机源、逐位可复现」现行已满足(七流=同一主种子的盐值分栏,非七个随机源);待裁决=甲维持分栏/乙物理并流(七消费点迁移+向量重生成+MC 基线作废)/丙并流+全事件日志;并流主要代价=分析面损失(存档未持久化流计数器)。 - -**归属**:[数据配置](systems/表现与支撑/数据配置.md) + [ADR-0007](../dev/architecture/adr-0007-rng-single-stream.md);节点图生成特性(拍板点②)实施排队其后 - -**状态**:已立项·ADR-0007 评估书已交付待裁决(2026-09-27) - - ### 教学引导 #### 教学形态未定 diff --git a/design/gdd/systems/冒险循环/关卡节点图生成.md b/design/gdd/systems/冒险循环/关卡节点图生成.md index 3003b188..ecd0a27c 100644 --- a/design/gdd/systems/冒险循环/关卡节点图生成.md +++ b/design/gdd/systems/冒险循环/关卡节点图生成.md @@ -1,7 +1,7 @@ # 关卡节点图生成 > 一句话:单关节点图怎么从固定配置变成逐局随机——图模板池、抽签与实例化 -> 状态:设计草案 v0.3(2026-09-27 立项;拍板点五项全部裁决落文;实施前置 = 全游戏随机流统一架构批裁决落地——见拍板点②与 ADR-0007) +> 状态:设计草案 v0.4(2026-09-27 立项;拍板点五项全部裁决落文;实施前置已解除——统一随机流批已落地〔ADR-0007 裁丙〕,抽签接入形态 = 统一流新用途标签 `node_map`〔扩签通道〕,待实施批) > 旧版参考:无(旧体系未单独成文;本事项自 2026-09-20 起登记 [06-待办清单](../../06-待办清单.md)「关卡内结构随机生成」条目) ## 设计目的 @@ -58,7 +58,7 @@ 模板池挂在关卡表逐关行上:行集从"单一节点集合"扩为"模板池"。**N=1 的池里只有现行那张图,逐局抽签退化回固定图**——这是**回滚档**(本文含义:任何一关把池缩回一张模板,该关行为就与今日固定配置逐位等价的保险形态)。整个设计因此可增量落地:先全关 N=1,再逐关扩池。 -抽签消费主种子派生的封闭命名流,流名提案 `node_map`(接入形态随拍板点②裁决:第八栏增流 or 统一流口径——架构批 [ADR-0007](../../../dev/architecture/adr-0007-rng-single-stream.md) 结论定)。 +抽签消费主种子派生的统一随机流,走扩签通道新立用途标签 `node_map`(统一流口径已定——[ADR-0007](../../../dev/architecture/adr-0007-rng-single-stream.md) 裁丙落地;标签不参与派生、只进事件日志)。 **内容纪律**:每张新模板等于一个小内容批——模板里每个战斗与 Boss 节点都必须在敌人与棋盘生成的数值表里有精确行,义务从冒险结构加关纪律的"每关一行集"细化到"每模板一行集";冒烟覆盖义务同批扩为遍历池内全部模板校验,不只抽中的那张。 @@ -171,7 +171,7 @@ ## P0 最小闭环(台阶一实施批) -> 实施前置:全游戏随机流统一架构批([ADR-0007](../../../dev/architecture/adr-0007-rng-single-stream.md))裁决落地——抽签随机量的接入形态(第八栏增流 or 并流口径)随其结论定。 +> 实施前置已解除:统一随机流批已落地([ADR-0007](../../../dev/architecture/adr-0007-rng-single-stream.md) 裁丙——统一流+全事件日志,2026-09-27);抽签接入形态 = 统一流扩签新用途标签 `node_map`(扩签通道 = 改代码 + 评审)。 1. 关卡表逐关行扩模板池结构(承载归数据配置,schema 细节随实施批定)。 2. 池内全部模板加载期预校验(违规致命,fail-fast 不变)。 @@ -218,3 +218,4 @@ | v0.1 | 2026-09-27 | **设计草案成文(立项批,用户拍板立项——06 条目「关卡内结构随机生成」随批改状态)**:概念三件套(图模板/模板池/图模板抽签,N=1 回滚档)、抽签时机(开始关卡内逐关抽、主种子派生 `node_map` 流提案)、三级台阶路线(抽签→属性随机→全程序)、MC 测量对账三条款(分层出数/基线可比/实施门=曲线调参批后)、五拍板点、依赖表与验收草案。实施未开工;术语入 registry 词汇表随定稿批执行 | | v0.2 | 2026-09-27 | **首轮裁决落文批(用户拍板①④⑤+路线确认)**:①第一关参与抽签、仅首次冒险走固定脚本——新增「首次冒险的固定脚本」节(存档域布尔标志「首次冒险已进行」、收束点置位、边界与测量口径,P0/验收/依赖表同步);④首版池规模=每关 5 张(调参表投值);⑤抽签时点确认;用户并裁「骨架不完全固定、多份随机调取」= 台阶一路线确认(台阶节补白话块+教学位随骨架走注记)。②③补证据面待复裁——新增「拍板点证据面」节:A=随机流/增流白话释义+两选项;B=台阶一详解(玩家看到什么/做三件/不做两件/内容形态 15 骨架/遭遇表按编号共用约束)+实施时点三选项(A 立即全上/B 等调参批/C 机制先行骨架随后,荐 C);测量对账实施门条款同步改"随机量扩池守门、机制可先行"口径 | | v0.3 | 2026-09-27 | **②③终裁落文批(用户两轮复裁)**:②统一用一个流·全面执行——全游戏 rng 架构批先行([ADR-0007](../../../dev/architecture/adr-0007-rng-single-stream.md) 评估书交付:核心发现=七流系同一主种子的盐值分栏、字面目标现行已满足;待裁决=甲维持分栏/乙物理并流/丙并流+全事件日志),本特性实施排队其后(P0 补前置行、抽签节流形态改随 ADR 结论);③特性内机制先行骨架随后(池暂 N=1、骨架内容批扩池);证据面 A 补裁决注记。06 新立「全游戏随机流统一架构批」条目(数据配置节) | +| v0.4 | 2026-09-27 | **实施前置解除批(统一随机流批随批——rng 架构批已按 ADR-0007 裁丙落地)**:状态行改实施解锁;抽签节流形态定稿 = 统一流扩签新用途标签 `node_map`(标签不参与派生、只进事件日志——并流后派生隔离概念退役);P0 闭环前置行改解除口径。本批只记架构依赖解除,特性实施仍未开工 | diff --git a/design/gdd/systems/冒险循环/冒险结构.md b/design/gdd/systems/冒险循环/冒险结构.md index 5fb968f0..7bb74995 100644 --- a/design/gdd/systems/冒险循环/冒险结构.md +++ b/design/gdd/systems/冒险循环/冒险结构.md @@ -172,7 +172,7 @@ GP(显示名「金币」)是冒险内的一次性属性提升货币:战斗 主动放弃按与失败同型结算:已获得的大奖励不追回、已到手的保留,摘要标记未打穿。放弃请求只在非战斗期受理,战斗进行中一律拒绝(战斗域单事务在飞)。放弃的界面入口挂在[关卡节点地图](关卡节点地图.md)的选路覆盖层上——选路停点天然是非战斗期,按钮只在冒险模式开放(选路覆盖层只在冒险模式出现);战斗进行中没有选路停点,入口因此天然不可达,拒绝分支仅作防御兜底。 -主种子(本文含义:冒险开始时注入的一个随机种子,本局全部随机性都派生自它)在冒险开始时注入恰一次。冒险内的雷图生成、奖励抽取、遭遇掷骰与 GP 掉落掷定(消费 gp_drop 封闭流)全部派生自它,冒险内不重置、不逐关播种——早期切片"每关开局各自播种"是没有冒险层时的历史简化,已改写为全冒险单一种子。冒险的可复现域 = 主种子 + 各随机流计数器,供复盘与快照(见[存档序列化](../表现与支撑/存档序列化.md))。重开抽到的种子与上一局相同是允许的(随机源范围小,撞种子不算错误),但生成新种子不得复用玩法里的随机数。 +主种子(本文含义:冒险开始时注入的一个随机种子,本局全部随机性都派生自它)在冒险开始时注入恰一次。冒险内的雷图生成、奖励抽取、遭遇掷骰与 GP 掉落掷定(消费 gp_drop 封闭流)全部派生自它,冒险内不重置、不逐关播种——早期切片"每关开局各自播种"是没有冒险层时的历史简化,已改写为全冒险单一种子。冒险的可复现域 = 主种子 + 全局取数计数器(统一随机流口径——ADR-0007 裁丙 2026-09-27 七流并一,语义权威归[数据配置](../表现与支撑/数据配置.md)),供复盘与快照(见[存档序列化](../表现与支撑/存档序列化.md))。重开抽到的种子与上一局相同是允许的(随机源范围小,撞种子不算错误),但生成新种子不得复用玩法里的随机数。 ### 与关卡节点地图的边界:收束点与接口四件套 @@ -286,3 +286,4 @@ GP(显示名「金币」)是冒险内的一次性属性提升货币:战斗 | v0.7 | 2026-09-21 | **放弃入口界面呈现批(用户拍板「放弃接口也要有」——台账 119,推翻本文原「MVP 无界面入口、呈现后置」条款)**:① 放弃入口挂[关卡节点地图](关卡节点地图.md)选路覆盖层(选路停点 = 天然非战斗期;战斗中拒绝转为防御兜底分支——入口在战斗期不可达);② 入口只在冒险模式开放,单关测试直入口没有可放弃的冒险对象;③ 正文改现行口径(放弃节 / 验收标准放弃条),P1 后置表「放弃入口的界面呈现」随落地移除(同表「两关序列生效」系 v0.6 已落地漏收口,顺手同口径移除);④ 验收闸 = ui_probe 三断言(默认隐藏 / 冒险模式可见 / 首战中按下被拒 state 保持 running)+ 冒烟既有放弃两态 API 断言 | | v0.8 | 2026-09-23 | **单关测试直入口退役(用户拍板——唯一试玩入口 = 完整三关冒险)**:正文改现行口径两处(放弃节「按钮只在冒险模式开放」的理由从「单关直入口无冒险对象」改为「选路覆盖层只在冒险模式出现」;验收标准放弃条同步去单关直入口从句)——放弃语义对象(整局冒险)与拒绝分支零改动。版本历史行(v0.7)按 ADR 纪律不回改 | | v0.9 | 2026-09-23 | **停点前移批流程口径对齐(用户 2026-09-23 拍板冒险入口流程倒置修复——与 HUD框架 / 关卡节点地图 / battle.md / `../../../dev/architecture/adr-0006-board-node2d-rendering.md` 同批)**:P0 闭环「开始关卡」条改新流程口径——关卡节点地图加载节点图后停在入口选路停点(关卡画面,选路覆盖层常驻形态),行进自玩家首次选路开始;通读复核其余章节——「通关自动进下一关」属关卡序列层语义(进入下一关后仍出图选路,零矛盾)、接口契约四件套、收束点先清库后宣布、放弃入口(选路停点 = 天然非战斗期)等既有裁决零改动。**批内代拍板三项已追认(2026-09-24 用户确认)**(字母不重编 / 28px 归 C/D 底行 / 进度呈现由选路覆盖层节点态承载)不落本文修订面,登记见 HUD框架 v0.8 | +| v0.10 | 2026-09-27 | **统一随机流批口径对齐(ADR-0007 裁丙——rng 架构批随批)**:主种子节「可复现域 = 主种子 + 各随机流计数器」改「主种子 + 全局取数计数器」(语义权威指数据配置);本文规则面零改动——主种子恰一次注入、重开即全新一轮、摘要五件事均不变。代码实证与七消费点迁移见数据配置 v0.5 | diff --git a/design/gdd/systems/表现与支撑/数据配置.md b/design/gdd/systems/表现与支撑/数据配置.md index 90738513..d34a0b95 100644 --- a/design/gdd/systems/表现与支撑/数据配置.md +++ b/design/gdd/systems/表现与支撑/数据配置.md @@ -19,7 +19,7 @@ | 负责 | 说明 | |---|---| | 全部数值表的结构与数据 | 表结构类(声明每列的类型、取值域与默认值)与数据文件成对交付;结构先行,数值由各回填责任方回填 | -| 全局读取口与随机服务 | 两个进程级单例:类型化读取口(代码标识符 ConfigDB)与封闭随机流服务(代码标识符 RngService) | +| 全局读取口与随机服务 | 两个进程级单例:类型化读取口(代码标识符 ConfigDB)与统一随机流服务(代码标识符 RngService) | | 加载、校验与热重载管线 | 启动加载、fail-fast 校验、调试构建热重载的状态管理 | | 进度坐标查行 | 进度键表(跟着冒险进度成长的表)按(关卡序号 × 节点深度)两维坐标解析取行 | | 表清单与对账 | 表路径固定、清单封闭;表实与 registry 逐字段对账 | @@ -31,7 +31,7 @@ | 各表数值定值与玩法公式 | 各回填责任方(清单见核心规则的表清单),平衡总归[难度曲线与Boss](../冒险循环/难度曲线与Boss.md) | | 存档格式与版本比较逻辑 | [存档序列化](存档序列化.md),本系统只出版本常量与随机快照契约 | | 界面呈现 | [HUD 框架](HUD框架.md) | -| 随机用途的设计合理性 | 消费方系统自证(随机须有界、可见或可推理),新增随机走增流通道 | +| 随机用途的设计合理性 | 消费方系统自证(随机须有界、可见或可推理),新增随机走扩签通道 | ## 在核心循环中的位置 @@ -75,21 +75,24 @@ 举证:本节为数据纪律约定,无计算。违规检测 = 静态扫描加人工复核,随各表回填定稿与里程碑检查执行。 -### 封闭随机流:随机只从一个服务按名取,同种子必同局 +### 统一随机流:随机只从一个服务按号取,同种子必同局 -全项目的随机数只经随机服务的封闭命名流发放。封闭随机流(本文含义:随机通道清单封闭,只含登记过的具名消费者;新增通道必须改代码并通过评审)是对"随机即失控"的制度化设防:玩法代码禁止自建随机源、禁止调用全局随机函数。目的只有一个——同一条主种子(master_seed,一场冒险的随机源头数字,由冒险结构在冒险开始时注入,仅全流计数器归零时受理)驱动下,相同决策序列必然跑出相同结果,这是回放、审计与自动化测试共同的立足点。 +全项目的随机数只经随机服务的统一流发放(ADR-0007 裁丙,2026-09-27 七流并一并加全事件日志)。统一流(本文含义:唯一主种子 + 唯一派生公式〔单盐〕+ 唯一全局取数计数器)是对"随机即失控"的制度化设防:玩法代码禁止自建随机源、禁止调用全局随机函数。目的只有一个——同一条主种子(master_seed,一场冒险的随机源头数字,由冒险结构在冒险开始时注入,仅全局计数器归零时受理)驱动下,相同决策序列必然跑出相同结果,这是回放、审计与自动化测试共同的立足点。 -现行流清单与取号时机: +取号按用途标签记账。用途标签(本文含义:取号时声明的用途名,只进事件日志、不参与种子派生)沿用并流前七条流名作封闭词表——标签让日志可读,也让"哪类事件在什么时机取号"仍保有登记位: -| 流名 | 消费方 | 用途:何时取号 | +| 用途标签 | 消费方 | 用途:何时取号 | |---|---|---| -| board_gen | [棋盘生成](../排雷循环/棋盘生成.md) | 每次生成新雷图(换图时)取号,布雷随机全部由此流供给 | +| board_gen | [棋盘生成](../排雷循环/棋盘生成.md) | 每次生成新雷图(换图时)取号,布雷随机全部由该次取号实例供给 | | reward_roll | [地图战斗资源](../战斗循环/地图战斗资源.md) | 每次装板时的奖励预分配批(格子奖励批 + 清图奖励批)取号 | | encounter | [战斗结算](../战斗循环/战斗结算.md) | 每场战斗实例化取号,掷骰决定敌方实际数量 | +| armor_roll / behavior_roll / variant_place / gp_drop | [战斗结算](../战斗循环/战斗结算.md) | 命中掷点逐次 / 开轮逐敌摇号 / 每板变异布置 / 每战末掉落掷定(结算表 armor_roll_stream 字段只作标签) | -取数有两个入口。玩法代码用取号入口:每调用一次,该流计数器加一并派生新种子,同一事件内共享同一实例。测试与预览用纯函数入口:按指定序号计算种子、不推计数器——预览不污染正式序列。种子由登记的派生公式计算,纯函数、跨引擎版本恒定,并登记了可复算的黄金向量。 +取数有两个入口。玩法代码用取号入口:每调用一次,全局计数器加一并派生新种子,同一事件内共享同一实例。测试与预览用纯函数入口:按指定全局序号计算种子、不推计数器——预览不污染正式序列。种子由登记的派生公式计算(单盐),纯函数、跨引擎版本恒定,并登记了可复算的黄金向量。 -生命周期:进程启动后流待命(未注入主种子就取数,致命中止,禁止默认种子兜底)→ 冒险开始注入主种子 → 冒险中按事件取号(计数器只增不重置)→ 冒险结束流保留供复盘。续玩与复盘的快照 = 主种子加各流计数器,由[存档序列化](存档序列化.md)消费;快照切出须发生在没有在用取号实例的时刻。要新增随机用途(含战斗内随机),必须走增流通道:改代码加流并通过评审——清单封闭是刻意的设卡。 +全事件日志(并流批新增):冒险开始即开日志会话(user://logs/rng_log_<主种子>.jsonl),每次取数落一行——全局序号、用途标签、派生种子。号位在全序账本下不再单独回答"第 N 次某类事件"(路线依赖为 ADR-0007 已裁决代价),单点定位与复盘由日志承载。写盘失败只告警降级,不致命、不影响玩法随机。 + +生命周期:进程启动后流待命(未注入主种子就取数,致命中止,禁止默认种子兜底)→ 冒险开始注入主种子并开日志会话 → 冒险中按事件取号(全局计数器只增不重置)→ 冒险结束流保留供复盘。续玩与复盘的快照 = 主种子加全局取数计数器,由[存档序列化](存档序列化.md)消费;快照切出须发生在没有在用取号实例的时刻。要新增随机用途(含战斗内随机),走扩签通道:改代码加用途标签并通过评审——词表封闭是刻意的设卡。 举证:派生公式、实现规约与黄金向量登记见 [registry 的 stream_seed_derivation 条目](../../../registry/entities.yaml),本文不转载。 @@ -154,7 +157,7 @@ | 下游 | [冒险结构](../冒险循环/冒险结构.md) | 关卡表与冒险表承载(登记现状见「待办」);主种子注入入口 | | 下游 | [难度曲线与Boss](../冒险循环/难度曲线与Boss.md) | 难度表承载(占位);进度坐标查行的消费方 | | 下游 | [元进度](../冒险循环/元进度.md) | 元进度表占位承载 | -| 下游 | [存档序列化](存档序列化.md) | 表结构版本常量;随机快照契约(主种子 + 各流计数器) | +| 下游 | [存档序列化](存档序列化.md) | 表结构版本常量;随机快照契约(主种子 + 全局取数计数器) | | 下游 | [HUD 框架](HUD框架.md) | 调试热重载完成通知(仅调试构建) | | 外发 | 无订阅 | 本系统不订阅任何玩法事件 | @@ -164,7 +167,7 @@ 2. 各回填责任方按系统定稿顺序回填数值。 3. 进程启动:逐表加载、逐表校验,错误收集后统一报错;全绿进入就绪态。 4. 全部系统经全局读取口读数,无第二取数路径。 -5. 冒险开始注入主种子,三条封闭流按各自事件取号。 +5. 冒险开始注入主种子并开事件日志会话,统一流按事件取号。 6. 调试构建热重载即改即验:战斗中被闸门拒绝,结束后手动再触发。 7. 无窗口冒烟脚本跑通加载、校验、取数、派生全链路,退出码 0。 @@ -186,7 +189,7 @@ |---|---|---| | 表路径清单 | data/tables/ 固定(调试注入根除外) | 冻结:散装路径破坏单点访问 | | 表结构版本常量 `SCHEMA_VERSION` | 实现期定(MVP = 1,provisional) | 全部表结构携带版本字段;高于当前支持的版本拒读,比较逻辑归[存档序列化](存档序列化.md) | -| 随机流清单 | 封闭三流(board_gen / reward_roll / encounter) | 冻结清单;增流 = 改代码 + 评审,有意设卡 | +| 随机流口径 | 统一流单盐单计数器 + 七用途标签封闭词表 | 统一口径为 ADR-0007 裁决;扩签 = 改代码 + 评审,有意设卡 | | 进度坐标解析策略 | 可取范围内向上取整 + 警告(保证偏难);超出最大行封顶取最大值 + 警告;域越界致命(2026-09-18 两段拍板,原「就近向下」退役) | 冻结:静默降难会掩盖难度断档 | | 入口锚行校验 | 写死开 | 非调参项;重议走文档修订流程 | | 调试覆盖开关 | 仅调试构建,默认关 | 正式构建结构性不存在;覆盖值永不回写表文件 | @@ -196,7 +199,7 @@ - 干净检出:无窗口冒烟脚本从干净检出启动,表清单内全部表加载成功且表目录无清单外文件;任一失败输出错误清单并退出非 0。 - 校验类别全覆盖:越界、文件缺失、空表、缺锚行、重复键、权重和为零、悬空引用各有一份测试夹具,逐类断言错误类别。 - 致命真的会死:任一致命夹具经正式通道加载,进程非 0 退出,而非仅"校验器能发现"。 -- 黄金向量:随机派生纯函数对登记向量逐个复算相等;同输入恒同输出;同流同序号重复调用恒等。 +- 黄金向量:随机派生纯函数对登记向量逐个复算相等;同输入恒同输出;同序号重复调用恒等。 - 计数器纪律:连续取号跨模拟关卡切换,计数器单调不重置;热重载成败都不改变主种子与计数器。 - 向上取整解析:请求无精确行时返回**不低于**请求坐标的最近行并留一条警告;请求超出最大已有行时封顶取最大行并留警告;同类警告去重;精确命中零警告。 - 越域与未播种:坐标低于合法域(关卡序号 <1、节点深度 <0)在进入解析前致命中止(Fatal 级诊断 + 进程退出码 1,切片探针 `smoke/config_fatal_probe.gd` 验证);高于最大已有行走封顶取最大行(警告留痕,不致命);未设主种子就取随机数致命中止,无默认种子兜底。 @@ -218,3 +221,4 @@ | v0.2 | 2026-09-15 | 雾格残留清理:关闭「待拍板」的棋盘生成参数表雾字段清理登记(fog_region_count / fog_target_ratio 两字段、雾占比绝对上限与雾域跨字段校验)——字段与校验随雾格移除同批删除,registry `board_gen_table` 条目同步修订。表清单与校验纪律不变 | | v0.3 | 2026-09-17 | 代币→钥石统一拍板随批措辞清偿:表清单冒险表行「代币记账」改「钥石入账」;registry adventure_table 同批改键 token_per_level→keystone_per_level。表清单与校验纪律不变 | | v0.4 | 2026-09-18 | **进度坐标解析口径修订(用户拍板,场景三讨论产物)**:① 取整方向「就近向下」→「**向上取整**」——无精确行取不低于请求坐标的最近行,理由 = 保证偏难(宁高估难度让工具读数保守,不静默降难掩盖断档);② 越域致命补齐——低于合法域(关卡序号 <1、节点深度 <0)走运行期致命:Fatal 级诊断 + 进程退出码 1,与装载期致命纪律并轨;**同日追加拍板**:高于最大已有行不致命,封顶取最大已有行并留警告(用户裁定「可取范围内向上取整,超过了最大值就用最大值」);③ registry enemy_table 条目「进度键 floor」随批改 ceil;切片 config_db._resolve_progress 同批落地(ceil 选择器 + _fatal 通道 + `smoke/config_fatal_probe.gd` 探针 + 冒烟 ceil 断言) | +| v0.5 | 2026-09-27 | **统一随机流批(ADR-0007 裁丙——用户两轮复裁终选「统一用一个流」并拍「全面执行」)**:§「封闭随机流」整节改「统一随机流」口径——唯一主种子+单盐派生+唯一全局取数计数器,七流并一;取号参数降格为**用途标签**(只进事件日志不参与派生,七流名保留为封闭词表);新增**全事件日志**(冒险会话制 user://logs/rng_log_<主种子>.jsonl,逐次取数落 scope/标签/派生种子——号位全序化后单点定位的承载面);快照契约=主种子+全局计数器;扩签通道替代增流通道;表清单/依赖表/P0/验收行随批。代码实证:rng_service.gd 重写+七消费点迁移+armor_roll_stream 字段降格标签+behavior_roll 子流派生向量退役;新黄金向量 8 常量重登记(scope 0-3 与旧 board_gen 向量逐位连续)| diff --git a/design/registry/entities.yaml b/design/registry/entities.yaml index cc67b38b..993a2291 100644 --- a/design/registry/entities.yaml +++ b/design/registry/entities.yaml @@ -20,35 +20,29 @@ entities: owner: design/gdd/systems/表现与支撑/数据配置.md attributes: kind: autoload - streams: [board_gen, reward_roll, encounter, armor_roll, behavior_roll, variant_place, gp_drop] - stream_names: 当前封闭清单(增流需改代码 + P1 支柱评审;combat_var 已于 2026-09-07 full 评审删除——玩法随机不预置基础设施;encounter 增流 2026-09-12 用户拍板——遭遇随机化批,scope_event_binding = 每次战斗实例化恰一 scope;**armor_roll 增流 2026-09-16 用户拍板**——护甲等级掷点批,回合内随机首次入流:scope_event_binding = 每次掷点恰一 scope〔逐次掷逐次取号、可逐位回放〕,消费面 = 战斗结算 吃掷点分量判定;**behavior_roll 增流 2026-09-16 用户拍板**——敌人技能池批(开轮摇号):回合内随机、时点前置到每轮开轮〔结算步骤流之外〕,scope_event_binding = 每敌每摇号恰一 scope,**派生向量 = spawn_index × 1000000 + 单敌摇号序**〔单敌出招轨迹可独立复放、敌互不穿插;取号门 = open_scope(stream, scope) 显式 scope 取号——计数器仍单调 +1 供观测〕,消费面 = 战斗结算 开轮摇号换算层 + 技能摇号器 SkillLottery;**variant_place 增流 2026-09-20 用户拍板**——地形变异首批类型定稿批:scope_event_binding = 每次装板恰一 scope〔常规换图/轮内刷新图各取号一次〕,消费面 = 战斗结算 布置段 _place_variants——变异类型选取 + 连通片生长取数;**gp_drop 增流 2026-09-20 用户拍板**——GP+商店房批:scope_event_binding = 每场战斗结束的掉落掷定恰一 scope,消费面 = 战斗结算 战斗结束分支——按战斗节点身份查商店表掷定 GP 掉落,掷定 + 关级加值 + 战斗内缓冲合成本战 gp_dropped〔shop_roll 流随"无三选一摇号"裁决作废、从未入册〕) - stream_facade: open_stream(stream) 取号(计数器+1→派生→返回已播种实例);derive_at(stream, scope) 纯函数供测试/预览(不推计数器) - new_adventure: 冒险开始重播种入口——调用权 = AdventureRunner.start_adventure 唯一正式调用方(切片单关测试路径 LevelRunner.start_level 除外;冒险结构实现批 2026-09-21——主种子全冒险恰一次注入,冒险内不重置不逐关播种) - scope_event_binding: board_gen=每次生成新扫雷图(棋盘生成);reward_roll=每次 board install(格奖励批 + 清图奖励批)(地图战斗资源——2026-09-09 接口漂移改写,原"每次奖励结算 roll 批次";地图战斗资源 Rule 2/AC-1 为契约源);encounter=每次战斗实例化恰一 scope(战斗结算 begin_battle 实例化期、扫雷开始前——unit_count 区间均匀骰,挖掘决策域之外 P1 合规);armor_roll=每次掷点恰一 scope(战斗结算 战斗段吃掷点分量逐次判定——1d20,护甲等级掷点批 2026-09-16);variant_place=每次装板恰一 scope(战斗结算 布置段——地形变异首批类型定稿批 2026-09-20);gp_drop=每场战斗结束的掉落掷定恰一 scope(战斗结算 战斗结束分支——GP+商店房批 2026-09-20) + stream_architecture: **统一流(ADR-0007 裁丙 2026-09-27——用户两轮复裁终选「统一用一个流」并拍「全面执行」,七流并一+全事件日志)**:唯一主种子 + 单盐派生 + 唯一全局取数计数器;历史注记 = 2026-09-12 encounter 增流〔遭遇随机化批〕/ 2026-09-16 armor_roll〔护甲掷点批〕+ behavior_roll〔敌人技能池批〕/ 2026-09-20 variant_place〔地形变异批〕/ 2026-09-21 gp_drop〔GP+商店房批〕——分盐分流架构随并流批整体退役;combat_var 已于 2026-09-07 full 评审删除(玩法随机不预置基础设施) + purpose_tags: [board_gen, reward_roll, encounter, armor_roll, behavior_roll, variant_place, gp_drop](封闭词表 = 并流前七流名全保留;扩签需改代码 + 评审;结算表 armor_roll_stream 字段降格为纯标签) + stream_facade: open_stream(purpose) 取号(purpose 只进事件日志不参与派生;全局计数器 +1 → 单盐派生 → 返回已播种实例 → 落日志);derive_at(scope) 纯函数供测试/预览/日志回放(不推计数器);open_scope 显式位取号门随子流派生向量退役(behavior_roll 逐敌顺序取号替代) + event_log: 冒险会话制全事件落盘——user://logs/rng_log_<主种子>.jsonl,会话行 + 逐次取数行(scope / 用途标签 / 派生种子);写盘失败告警降级不致命;号位全序化后不再单独回答「第 N 次某类事件」(路线依赖为已裁决代价),单点定位由日志承载 + new_adventure: 冒险开始重播种入口——调用权 = AdventureRunner.start_adventure 唯一正式调用方(切片单关测试路径 LevelRunner.start_level 除外;冒险结构实现批 2026-09-21——主种子全冒险恰一次注入,冒险内不重置不逐关播种);并流批加重播种同时开日志会话 + tag_event_binding: board_gen=每次生成新扫雷图(棋盘生成);reward_roll=每次 board install(地图战斗资源);encounter=每次战斗实例化(战斗结算 begin_battle 实例化期、扫雷开始前——**实证先于 board_gen 取号**);armor_roll=每次掷点(战斗结算 1d20);behavior_roll=每敌每轮开轮摇号(战斗结算——并流后顺序取号,spawn_index×STRIDE 派生向量退役);variant_place=每次装板(战斗结算 布置段);gp_drop=每场战斗结束掉落掷定(战斗结算)——标签绑定只描述取号时机语义,不再承载派生隔离 unseeded_access: 致命中止 - snapshot_contract: 续玩/复盘快照 = master_seed + 各流 scope 计数器(消费方 存档序列化) + snapshot_contract: 续玩/复盘快照 = master_seed + 全局取数计数器(消费方 存档序列化;并流前「各流计数器」口径退役——现行存档未持久化计数器,契约面登记变更) referenced_by: [棋盘生成, 战斗结算, 地图战斗资源, 冒险结构, 存档序列化, 关卡节点图生成] - - name: stream_seed_derivation type: formula owner: design/gdd/systems/表现与支撑/数据配置.md attributes: - variables: [SALT_s, master_seed, scope] + variables: [SINGLE_SALT, master_seed, scope] output_range: "[0, 2^63)" - golden_vectors: 见《数据配置》Formulas-R3 向量表(8 常量——2026-09-19 掷点测量修复批 B 案 R2→R3 改版全量重登记,Python 参考实现逐位交叉验证:board_gen m=42 scope0=8672514112661456564 / scope1=8834291164406037215 / scope2=7714373262647006282 / scope3=1280102845971471347 · reward_roll m=42 scope0=8363815839763232861 · encounter m=42 scope0=5906263231575069166 · armor_roll m=42 scope0=1459964581993244806 · behavior_roll m=42 scope0=1791557311233092846 · variant_place m=42 scope0=5785725196205023206——2026-09-20 地形变异首批类型定稿批 variant_place 增流按 R3 公式补向量;R2 旧向量随批作废;**2026-09-21 GP+商店房实施批 gp_drop 增流向量补登**:gp_drop m=42 scope0=6226476397888000011 / scope1=5031558177015478025——按 R3 公式登记,冒烟同批断言) - salt_board_gen: 0xA0761D6478BD642F - salt_reward_roll: 0xE7037ED1A0B428DB - salt_encounter: 0x8EBC6AF09C88C6E3(2026-09-14 encounter 增流——互异奇常数 ≠ 乘数) - salt_armor_roll: 0xC2B2AE3D27D4EB2F(2026-09-16 armor_roll 增流——互异奇常数 ≠ 乘数) - salt_behavior_roll: 0x85EBCA7758C24E15(2026-09-16 behavior_roll 增流——互异奇常数 ≠ 乘数) - salt_variant_place: 0x9AE16A3F2FAD6C39(2026-09-20 variant_place 增流——互异奇常数 ≠ 乘数) - salt_gp_drop: 0xD6E8FEB8665FD7F1(2026-09-21 gp_drop 增流——互异奇常数 ≠ 乘数;GP+商店房实施批随黄金向量同批补登) + golden_vectors: 统一流 8 常量(**2026-09-27 统一随机流批 ADR-0007 裁丙单盐重登记**——Python 参考实现逐位交叉验证;m=42:scope0=8672514112661456564 / scope1=8834291164406037215 / scope2=7714373262647006282 / scope3=1280102845971471347 / scope4=82585501446482399 / scope5=8078924421474470922 / scope6=6638052740430622424 / scope7=8264196488417721294;scope 0-3 与并流前 board_gen 向量逐位连续〔单盐沿用原 board_gen 盐〕;并流前分盐向量 10 常量随批作废) + salt: 0xA0761D6478BD642F(统一单盐——沿用并流前 board_gen 盐;历史分盐 salt_reward_roll/encounter/armor_roll/behavior_roll/variant_place/gp_drop 六常量随批退役) multiplier: 0x9E3779B97F4A7C15 - finalizer_mix: splitmix64 finalizer 变体(2026-09-19 B 案新增——derive = mix64(SALT XOR wrap64(master + scope×GOLDEN));乘数 0xBF58476D1CE4E5B9 / 0x94D049BB133111EB、加数同 GOLDEN_RATIO;全程 63 位掩码;动机=窄种子窗×短局复用会把裸派生种子头部窗口的面值彩票放大进聚合统计,混淆使任何窄种子窗表现如随机抽样) + finalizer_mix: splitmix64 finalizer 变体(2026-09-19 B 案——derive = mix64(SINGLE_SALT XOR wrap64(master + scope×GOLDEN));乘数 0xBF58476D1CE4E5B9 / 0x94D049BB133111EB、加数同 GOLDEN_RATIO;全程 63 位掩码;动机=窄种子窗×短局复用会把裸派生种子头部窗口的面值彩票放大进聚合统计,混淆使任何窄种子窗表现如随机抽样) impl_spec: wrap64=二补数自然回绕;禁 float 中间量、禁对中间量用 %、禁"主动修复溢出";常量用分段表达式书写;掩码收尾;mix64 内全程 OUT_MASK 63 位掩码(有符号 int64 算术移位在 63 位正值域与逻辑移位逐位一致——参考实现对齐关键) - reproducibility_claim: 同冒险内同流各 scope 无碰撞 + 跨版本恒定(不做跨 master_seed 全局单射声明) + reproducibility_claim: 同冒险内全局各 scope 无碰撞 + 跨版本恒定(不做跨 master_seed 全局单射声明) master_seed_int64_min: 致命(abs 恒等回绕→与 0 静默别名) - set_master_seed_precondition: 仅全流计数器全零时受理(7 流——2026-09-14 数据配置 漂移批口径 + 2026-09-16 护甲等级掷点批 armor_roll 增流 + 同日敌人技能池批 behavior_roll 增流 + 2026-09-20 地形变异首批类型定稿批 variant_place 增流 + 同日 GP+商店房批 gp_drop 增流) + set_master_seed_precondition: 仅全局计数器归零时受理(并流批口径——原「7 流全零」退役) referenced_by: [棋盘生成, 战斗结算, 地图战斗资源, 冒险结构, 存档序列化, 关卡节点图生成] - name: difficulty_row_resolution diff --git a/game/core/battle_orchestrator.gd b/game/core/battle_orchestrator.gd index 57a68c52..60ef62b6 100644 --- a/game/core/battle_orchestrator.gd +++ b/game/core/battle_orchestrator.gd @@ -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 拍不可摇中"的等价实现(单位容器 注记) diff --git a/game/core/rng_service.gd b/game/core/rng_service.gd index 12f99f0b..4e75a265 100644 --- a/game/core/rng_service.gd +++ b/game/core/rng_service.gd @@ -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_.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) diff --git a/game/core/unit_container.gd b/game/core/unit_container.gd index 9d2b1a13..1ad625c0 100644 --- a/game/core/unit_container.gd +++ b/game/core/unit_container.gd @@ -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 失效句柄") diff --git a/game/data/schemas/settlement_table.gd b/game/data/schemas/settlement_table.gd index 83c95130..b597056f 100644 --- a/game/data/schemas/settlement_table.gd +++ b/game/data/schemas/settlement_table.gd @@ -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"] diff --git a/game/smoke/damage_probe.gd b/game/smoke/damage_probe.gd index 9fc6a737..6d86aa07 100644 --- a/game/smoke/damage_probe.gd +++ b/game/smoke/damage_probe.gd @@ -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 diff --git a/game/smoke/skill_rhythm_probe.gd b/game/smoke/skill_rhythm_probe.gd index 5410ec96..f607d1d1 100644 --- a/game/smoke/skill_rhythm_probe.gd +++ b/game/smoke/skill_rhythm_probe.gd @@ -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]: diff --git a/game/smoke/smoke_all.gd b/game/smoke/smoke_all.gd index 7c2fc9d3..939f3dc2 100644 --- a/game/smoke/smoke_all.gd +++ b/game/smoke/smoke_all.gd @@ -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 归元进度设计批配读数; diff --git a/game/smoke/ui_input_probe.gd b/game/smoke/ui_input_probe.gd index c8897433..cf092f21 100644 --- a/game/smoke/ui_input_probe.gd +++ b/game/smoke/ui_input_probe.gd @@ -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: