跳到正文

随机生成器携带状态。取一个值会推进状态,重新播种则会替换状态。互不相关的系统共享生成器时,它们的历史会在下一次抽取处相遇。

随机流不止一条

游戏自有的全局生成器、库打乱生成器与对象自有生成器,各有独立状态。进程还会从 random_device 初始化另一个引擎。把它们当成一条序列,会得到错误的依赖关系。

全局游戏生成器从种子 4357 开始,原始结果有 31 位。有界整数通过取模产生,浮点辅助函数则缩放一个值,再舍入为 float32。11共享游戏生成器游戏自有 MT19937 从 4357 开始,返回低 31 位,有界整数取模,浮点抽取除以 2147483647.0 后转换为 float32。分支清点找到 38 处全局播种调用,其中 30 处传入 time(NULL)。此外还存在对象自有生成器与使用 random_device 播种的进程引擎。这些游戏播种不影响 libc++ 打乱引擎。0x1034ff9440x103608ff40x1036090b40x10360921c

直到 4.2.4.399 版本,进化的结果选择调用 std::random_shuffle。其库引擎与摩登传送门队列等其他列表打乱共享。重置游戏的全局生成器,不会重置这个库引擎。22独立的 libc++ 打乱随机流导入的 __rs_get 与 __rs_default 符号标识双参数 std::random_shuffle。前向排列通过带拒绝采样的 uniform_int_distribution 取值。PlayCover 实时捕获记录了匹配种子 5489 的 mt19937 原始输出、完整排列与选择;宿主库检查属于独立证据。记录的 iPad 结果印证指定路线,但未识别 iPad 加载的库。进化与摩登传送门共享该库随机流。0x1009a92800x1001210440x10001bf384.2.6.401 版本改用神器专用的生成器选择进化结果,详见4.2.6.401 版本中的进化神器选择。

图版 13.1以 4357 为种子的游戏全局生成器,与以 5489 为种子的库 mt19937 引擎:每格是一次原始输出,按顺序排列。重新播种传入时钟秒数 1790000000。

分步查看

  1. 1两条序列。 只有当某处从生成器取值时,它才会前进。
  2. 2游戏抽取。 从游戏生成器取出三个值,使它前进到位置 3;库引擎没有移动。
  3. 3一次库打乱。 打乱古埃及的四槽传送门列表,消耗库引擎的 3 个输出;游戏生成器没有移动。
  4. 4按时钟重新播种。 以 time(NULL) = 1790000000 为游戏生成器播种,会替换它的状态:它从该种子的第一个输出重新开始。库引擎保持原来的位置。

多抽一次,下一个答案就移动了

设想一个生成器的接下来几个值,分别交给手套概率、掉落概率与放置选择。若另一项操作在掉落之前消耗了一个值,掉落例程就会拿到序列中的另一部分。

两个功能之间不需要直接调用,共享状态已经足够。如果抽取发生了,即使结果没被使用,生成器也已经前进。

击杀奖励例程说明了这一点:即使植物没有对应奖励概率,它仍可能抽取。神器分支也可能先抽出数值,再覆盖它们。一个功能消耗的抽取次数,也是它影响后续消耗者的一部分。

打乱还增加了另一项变量。排列含 nn 项的列表需要 n−1n-1 次分布调用,而拒绝采样每次调用可以消耗不止一个引擎值。因此,改变候选数量也可以改变随机流前进的距离。

图版 13.2从种子 4357 开始的游戏全局生成器,其后续输出依次交给本章设想的三个消耗者。

分步查看

  1. 1原计划。 手套概率、掉落概率与放置选择依次取走第 0、1、2 个输出。
  2. 2多抽一次。 另一项操作先取走第 1 个输出:掉落概率改为得到第 2 个输出,放置选择得到第 3 个,尽管两个功能互不调用。

多个功能重置同一个生成器

共享游戏生成器有 38 处重新播种调用,其中 30 处传入 time(NULL)。这是横跨植物、掉落与神器的模式。时钟分辨率为一秒,每次重新播种都会替换生成器已经累积的状态。33跨系统共享的时钟重新播种分支清点找到共享播种封装的 38 处调用,其中 30 处传入 time(NULL)。已对应的时钟调用方包括加权掉落选择器、金币泉同类例程、扭蛋抽取、三份指令等价的僵王掉落实现,以及两条地锯草路径。掉落选择器在有界抽取前立即播种;扭蛋在抽取入口、后续选择逻辑之前播种。这些位置都替换 0x104e3e580 的状态。第二次播种会丢弃此前插入的抽取;只有该次播种之后的抽取,才会移动后续消耗者的位置。libc++ 打乱引擎是另一个对象。0x1034fca380x10198019c0x10197f7a40x1010952480x1000e172c0x101eeab000x10229a9c80x100b21ec80x100b24b1c

调用方重置发生的位置
加权掉落选择器紧接有界加权抽取之前
金币泉掉落相关的掉落选择例程中
扭蛋神器抽取入口,后续抽取之前
僵王掉落同一掉落重写的三份实现中
地锯草攻击检查可选抽取所需等级之前
地锯草暴击紧接有界抽取之前

这些调用方都重置同一个对象。因此,掉落例程可以改变植物或神器之后读取的随机状态,正如植物可以改变后续掉落使用的状态。

两次相同范围的抽取,若各自紧接相同秒数的重新播种,就会得到相同首项。两次重置之间的抽取会被第二次重置丢弃。在第二次重置之后、下一个消耗者之前发生的抽取,才会改变该消耗者收到的输出。

地锯草还多了一处顺序错误

地锯草攻击例程先重新播种,再检查植物是否为等级 5。低于该等级时,它不进行可选抽取:改变了共享状态,却没有自己使用一个值。44攻击例程重置其他系统的随机状态地锯草攻击例程先调用 time(NULL) 并重新播种,再检查植物是否为等级 5。较低等级分支在重置后不进行抽取。其特效对象暴击路径也在 bounded-100 抽取之前立即播种。相同种子产生相同首项;播种后插入的其他抽取可以改变某个消耗者拿到的输出。0x100b21e980x100b21ec80x100b248980x100b24b1c

把等级检查提前,可以消除这一处不取值的重置,却不能修复更广泛的关联:其他使用时钟种子的调用方仍会替换共享状态。共同问题在于局部功能重置了进程级依赖。

图版 13.3以 time(NULL) = 1790000000 重新播种游戏生成器后立即进行的抽取,上界为 100。每格是重新播种后状态的一次原始输出。

分步查看

  1. 1重新播种,然后抽取。 某个调用方以 1790000000 重新播种并立即抽取:该状态的第 0 个输出在上界 100 下得到 66。
  2. 2中间的抽取。 其他消耗者取走同一状态的第 1、2 个输出。
  3. 3同一秒内再次重新播种。 另一个调用方以 1790000000 重新播种:状态被替换,第 1、2 个输出不再起作用,它以相同上界进行的第一次抽取重复得到 66。
  4. 4下一秒。 以 1790000001 重新播种会开始另一条序列;它的第一次抽取得到 21。

好的生成器也修不好错误的选择循环

扭蛋神器的五项选择器构造累积权重,却在访问每项时重新抽取。这与只抽一次、再找到该值所在区间不同。

累积总量为 CiC_i、总权重为 TT 时,其理想化选择份额为

P(i)=[∏j<i(1−CjT)]CiT.P(i)=\left[\prod_{j<i}\left(1-\frac{C_j}{T}\right)\right]\frac{C_i}{T}.

五项等权时,份额为 20%、32%、28.8%、15.36%、3.84%。输入权重相等,输出频率却不相等。55在累积扫描内部重新抽取五项选择器构造累积权重,但循环回边返回随机抽取位置。第 i 项的份额为此前各项 (1-Cj/T) 的乘积再乘 Ci/T。五项等权时,理想化份额为 0.2、0.32、0.288、0.1536、0.0384。等权示例展示算法,不把尚未对应的资源权重向量当作此处输入。浮点区间包含上端点,还允许一个未选中任何项的剩余情况。0x1010952240x1010953e40x10109540c

权重应得的占比循环给出的占比
20%
32%
28.8%
15.36%
3.84%
条目 1条目 2条目 3条目 4条目 5
图版 13.4扭蛋的五条目选择器,权重为 1, 1, 1, 1, 1:它构建了累计权重,却在访问每个条目时重新抽取,因此给出的占比与权重应得的占比不同。P(2) = (1 − C₁/T) × C₂/T = (1 − 1/5) × 2/5 = 32%

这种偏差远大于普通小范围取模时的微小偏差。累计权重选择应当只抽取一次,再寻找包含该值的区间;在每次比较时重新抽取会改变分布。

注释

每条论断的依据:代码路径,以及追踪到它的原生地址。

  1. 1

    共享游戏生成器

    游戏自有 MT19937 从 4357 开始,返回低 31 位,有界整数取模,浮点抽取除以 2147483647.0 后转换为 float32。分支清点找到 38 处全局播种调用,其中 30 处传入 time(NULL)。此外还存在对象自有生成器与使用 random_device 播种的进程引擎。这些游戏播种不影响 libc++ 打乱引擎。

    0x1034ff9440x103608ff40x1036090b40x10360921c
  2. 2

    独立的 libc++ 打乱随机流

    导入的 __rs_get 与 __rs_default 符号标识双参数 std::random_shuffle。前向排列通过带拒绝采样的 uniform_int_distribution 取值。PlayCover 实时捕获记录了匹配种子 5489 的 mt19937 原始输出、完整排列与选择;宿主库检查属于独立证据。记录的 iPad 结果印证指定路线,但未识别 iPad 加载的库。进化与摩登传送门共享该库随机流。

    0x1009a92800x1001210440x10001bf38
  3. 3

    跨系统共享的时钟重新播种

    分支清点找到共享播种封装的 38 处调用,其中 30 处传入 time(NULL)。已对应的时钟调用方包括加权掉落选择器、金币泉同类例程、扭蛋抽取、三份指令等价的僵王掉落实现,以及两条地锯草路径。掉落选择器在有界抽取前立即播种;扭蛋在抽取入口、后续选择逻辑之前播种。这些位置都替换 0x104e3e580 的状态。第二次播种会丢弃此前插入的抽取;只有该次播种之后的抽取,才会移动后续消耗者的位置。libc++ 打乱引擎是另一个对象。

    0x1034fca380x10198019c0x10197f7a40x1010952480x1000e172c0x101eeab000x10229a9c80x100b21ec80x100b24b1c
  4. 4

    攻击例程重置其他系统的随机状态

    地锯草攻击例程先调用 time(NULL) 并重新播种,再检查植物是否为等级 5。较低等级分支在重置后不进行抽取。其特效对象暴击路径也在 bounded-100 抽取之前立即播种。相同种子产生相同首项;播种后插入的其他抽取可以改变某个消耗者拿到的输出。

    0x100b21e980x100b21ec80x100b248980x100b24b1c
  5. 5

    在累积扫描内部重新抽取

    五项选择器构造累积权重,但循环回边返回随机抽取位置。第 i 项的份额为此前各项 (1-Cj/T) 的乘积再乘 Ci/T。五项等权时,理想化份额为 0.2、0.32、0.288、0.1536、0.0384。等权示例展示算法,不把尚未对应的资源权重向量当作此处输入。浮点区间包含上端点,还允许一个未选中任何项的剩余情况。

    0x1010952240x1010953e40x10109540c