[P0][T1-4] 消除 DRLGROOMTILE_GetTileCache 中的 Math.random,强制注入确定性 seed #78

Closed
opened 2026-09-18 09:47:26 +00:00 by troytt · 1 comment
Owner

问题

src/game/wilderness.ts:1439 在 DRLGROOMTILE_GetTileCache() 内部存在一条 Math.random() 兜底路径:

// wilderness.ts:1424-1440
let roll: number
if (query?.seed !== undefined) {
  ...
} else {
  roll = Math.random()          // <-- 非确定性
}

scripts/verify-generators.ts:539 已经把它判为失败,当前 gate 是红的:

- src/game/wilderness.ts: calls Math.random

为什么这是 P0

原版 DRLG 只使用 SEED_RollRandomNumber / SEED_RollLimitedRandomNumber
(1.13c 中表现为内联的 LCG,乘数 0x6AC690C5,见 0x6FD9257A)。
整个关卡生成是「给定 seed ⇒ 唯一确定的地图」。

只要有任何一条调用路径没传 seed,同一个 seed 就可能生成不同的地图。这会:

  1. 破坏存档/联机的地图一致性(D2 的核心契约);
  2. 让 verify-packs 的 1671 项逐字节基线随时可能假红/假绿;
  3. 让任何「同 seed 复现 bug」的排查手段失效。

修改方案

不要简单地把 Math.random() 换成一个固定常量——那会把非确定性变成静默的错误分布。

  1. 把 seed 从可选改为必填:
    export interface TileCacheQuery {
      readonly seed: number | Rng      // 去掉 `?` 和 `| undefined`
      ...
    }
    export function DRLGROOMTILE_GetTileCache(
      libraries: readonly Dt1[],
      dt1Mask: number,
      query: TileCacheQuery,           // 去掉 `?`
    ): TileCacheResult | null
    
  2. 让编译器找出所有调用点。tsc 报出的每一处都要显式决定 seed 来源:
    • 生成期调用 ⇒ 传当前关卡的 Rng 实例(与原版一致,从 level seed 流里取);
    • 若某调用点确实拿不到 seed,说明调用链设计有问题,要把 Rng 透传下去,不要就地造一个。
  3. 删除 else { roll = Math.random() } 分支,连同 query?. 的可选链。
  4. 保留 cellHash(x, y, seed) 那条路径不变——它已经是确定性的。

[!TIP]
若某些测试桩调用不关心具体瓦片,传 0 或一个固定的 Rng(0) 即可,但必须显式写出来,
让「这里用了固定 seed」在代码里可见,而不是藏在默认值里。

验收标准

  • npx tsx scripts/verify-generators.ts 不再出现 calls Math.random
  • 全库检索 Math.random 在 src/game/ 下零命中
  • TileCacheQuery.seed 为必填,DRLGROOMTILE_GetTileCache 第三参数为必填
  • typecheck 0 error
  • vitest 全绿(当前基线 1127 passed / 69 files / 2 skipped)
  • verify-packs 保持 1671/1671
  • 新增回归测试:同一 seed 调用两次 DRLGROOMTILE_GetTileCache 返回完全相同的结果;
    不同 seed 在足够样本下能取到不同结果(防止有人用固定常量糊弄过去)

溯源

审计报告 113c-divergence-audit.md(条目编号与本 issue 标题中的 T 编号对应)
黄金标准 1.13c D2Common.dll,PE 时间戳 0x4B95C439 = 2010-03-09,ImageBase 0x6FD50000
可读代理 D2MOO 源码为 1.10f,非 1.13c。已通过「6/6 常量表命中 + 两个函数反汇编控制流比对」验证其 DRLG 部分与 1.13c 一致,详见报告开头「代理有效性验证」

[!WARNING]
引用 D2MOO 行号时请注意它是 1.10f。涉及 RNG 消耗次数或精确常量的改动,必须回到 1.13c 二进制复核
(工具:scratch/probe_113c_drlg.py、scratch/xref.py、scratch/findbytes.py + objdump)。

## 问题 `src/game/wilderness.ts:1439` 在 `DRLGROOMTILE_GetTileCache()` 内部存在一条 `Math.random()` 兜底路径: ```ts // wilderness.ts:1424-1440 let roll: number if (query?.seed !== undefined) { ... } else { roll = Math.random() // <-- 非确定性 } ``` `scripts/verify-generators.ts:539` 已经把它判为失败,当前 gate 是红的: ``` - src/game/wilderness.ts: calls Math.random ``` ## 为什么这是 P0 原版 DRLG **只**使用 `SEED_RollRandomNumber` / `SEED_RollLimitedRandomNumber` (1.13c 中表现为内联的 LCG,乘数 `0x6AC690C5`,见 `0x6FD9257A`)。 整个关卡生成是「给定 seed ⇒ 唯一确定的地图」。 只要有**任何一条**调用路径没传 `seed`,同一个 seed 就可能生成不同的地图。这会: 1. 破坏存档/联机的地图一致性(D2 的核心契约); 2. 让 `verify-packs` 的 1671 项逐字节基线随时可能假红/假绿; 3. 让任何「同 seed 复现 bug」的排查手段失效。 ## 修改方案 **不要**简单地把 `Math.random()` 换成一个固定常量——那会把非确定性变成静默的错误分布。 1. **把 `seed` 从可选改为必填**: ```ts export interface TileCacheQuery { readonly seed: number | Rng // 去掉 `?` 和 `| undefined` ... } export function DRLGROOMTILE_GetTileCache( libraries: readonly Dt1[], dt1Mask: number, query: TileCacheQuery, // 去掉 `?` ): TileCacheResult | null ``` 2. **让编译器找出所有调用点**。`tsc` 报出的每一处都要显式决定 seed 来源: - 生成期调用 ⇒ 传当前关卡的 `Rng` 实例(与原版一致,从 level seed 流里取); - 若某调用点确实拿不到 seed,说明调用链设计有问题,**要把 Rng 透传下去**,不要就地造一个。 3. **删除 `else { roll = Math.random() }` 分支**,连同 `query?.` 的可选链。 4. 保留 `cellHash(x, y, seed)` 那条路径不变——它已经是确定性的。 > [!TIP] > 若某些测试桩调用不关心具体瓦片,传 `0` 或一个固定的 `Rng(0)` 即可,但**必须显式写出来**, > 让「这里用了固定 seed」在代码里可见,而不是藏在默认值里。 ## 验收标准 - [ ] `npx tsx scripts/verify-generators.ts` 不再出现 `calls Math.random` - [ ] 全库检索 `Math.random` 在 `src/game/` 下零命中 - [ ] `TileCacheQuery.seed` 为必填,`DRLGROOMTILE_GetTileCache` 第三参数为必填 - [ ] typecheck 0 error - [ ] vitest 全绿(当前基线 1127 passed / 69 files / 2 skipped) - [ ] `verify-packs` 保持 1671/1671 - [ ] **新增回归测试**:同一 seed 调用两次 `DRLGROOMTILE_GetTileCache` 返回完全相同的结果; 不同 seed 在足够样本下能取到不同结果(防止有人用固定常量糊弄过去) --- ### 溯源 | | | |---|---| | 审计报告 | `113c-divergence-audit.md`(条目编号与本 issue 标题中的 T 编号对应) | | 黄金标准 | 1.13c `D2Common.dll`,PE 时间戳 `0x4B95C439` = 2010-03-09,ImageBase `0x6FD50000` | | 可读代理 | D2MOO 源码为 **1.10f**,非 1.13c。已通过「6/6 常量表命中 + 两个函数反汇编控制流比对」验证其 DRLG 部分与 1.13c 一致,详见报告开头「代理有效性验证」 | > [!WARNING] > 引用 D2MOO 行号时请注意它是 1.10f。涉及 RNG 消耗次数或精确常量的改动,**必须**回到 1.13c 二进制复核 > (工具:`scratch/probe_113c_drlg.py`、`scratch/xref.py`、`scratch/findbytes.py` + `objdump`)。
troytt added this to the [M17] 核心地下城与碰撞层原版一致性 (1.13c) milestone 2026-09-18 09:47:26 +00:00
Author
Owner

验证与修复总结 (Issue #78)

  1. 核心逻辑修复:
    • TileCacheQuery.seed 字段改为必填 (readonly seed: number | Rng),彻底移除可选标记与 | undefined。
    • DRLGROOMTILE_GetTileCache 中的 query 参数改为必填。
    • 彻底删除内部 else { roll = Math.random() } 兜底分支,强制要求传入明确的随机种子或 Rng。
  2. 测试与回归防护:
    • tests/tile-cache-seed.test.ts 新增 7 个回归测试,验证相同种子/坐标/Rng 具有完全一致的确定性输出,不同种子满足稀有度分布多样性。
    • 适配既有单元测试,显式注入确定性 seed: 0。
    • scripts/verify-generators.ts 的 calls Math.random 违规消除。
  3. 验证门禁全部通过:
    • npm run typecheck: 0 错误
    • npx vitest run: 1134 passed, 2 skipped
    • npx tsx scripts/verify-packs.ts: 1671/1671 项断言全绿
  4. 代码提交与合并:
    • Commit: 3fba5e91367d2c46a0538810ad9522ca40cd1f6f
    • 已 fast-forward 合并至 main 并推送至 origin/main。
### 验证与修复总结 (Issue #78) 1. **核心逻辑修复**: - `TileCacheQuery.seed` 字段改为必填 (`readonly seed: number | Rng`),彻底移除可选标记与 `| undefined`。 - `DRLGROOMTILE_GetTileCache` 中的 `query` 参数改为必填。 - 彻底删除内部 `else { roll = Math.random() }` 兜底分支,强制要求传入明确的随机种子或 Rng。 2. **测试与回归防护**: - `tests/tile-cache-seed.test.ts` 新增 7 个回归测试,验证相同种子/坐标/Rng 具有完全一致的确定性输出,不同种子满足稀有度分布多样性。 - 适配既有单元测试,显式注入确定性 `seed: 0`。 - `scripts/verify-generators.ts` 的 `calls Math.random` 违规消除。 3. **验证门禁全部通过**: - `npm run typecheck`: 0 错误 - `npx vitest run`: 1134 passed, 2 skipped - `npx tsx scripts/verify-packs.ts`: 1671/1671 项断言全绿 4. **代码提交与合并**: - Commit: `3fba5e91367d2c46a0538810ad9522ca40cd1f6f` - 已 fast-forward 合并至 `main` 并推送至 `origin/main`。
Sign in to join this conversation.
No Label
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: troytt/diablo2-web#78
No description provided.