今天在刷小黑盒时,无意间看到篇关于推测小黑盒 UID 生成算法的文章——
这让我想起,一年半前做 Minecraft 地图那会儿,我也曾对着 UID 发过呆:
“能不能搞个非自增式的 UID 系统?……虽然但……好像和自增也没啥本质区别,还更麻烦——
但!它好看啊 (✧∀✧)”
当时能力不够,硬是想破头也没整出个像样的。倒是误打误撞,搞了个「基于 UID + 时间戳」的伪随机运气值算法:
每日固定、无需存储,一人一号一天一运——灵感来自一个 QQ 机器人(记得是“QQ 号 × 时间戳 % 101”大概)。
效果嘛,真·算命:你今天的欧气,是数学算出来的,不是抽出来的…
如今回看,当年卡住我的,其实是同一个问题的不同侧面:
- 运气值:要的是「确定性伪随机」——输入(uid+时间)→ 输出(固定运气)
- UID 生成:要的是「无状态唯一性」——输入(上一个 uid)→ 输出(下一个 uid),且不重、不小、不溢出
而这次,我想把它真正走通。
限制 & 初始思路
mc 原版的计分板,是我给自己设的限制:
运算只有 + - × ÷ % 交换 比较,没有数组、没有数据库、不能遍历、不能存历史——
换言之:你只能用上一个 UID,和几个常数,算出下一个。
那最朴素的骨架自然浮现:
next_uid = (current × A) % M其中:
M很好定:int32 正整数上限 = (一个质数,记作 )- 但是
A不能随便选,选错了序列可能:- 很快撞车(周期短)
- 卡在 0 不动(死循环)
- 或者……前几百个看着“随机”,一千个后就开始跳 1,2,3……(周期性崩坏)
于是问题转为:
什么样的
A,能让这个乘法模运算是“走最长的路还不回头”的?
从“试试看”到“为什么”
我一开始也想:要不……写个脚本枚举 A=2,3,4… 看哪个周期长?
懒是一回事,更怕的是:就算试出一个“周期够 5 万”,我也不知道它为什么好,哪天崩了也说不出原因。
得往数学深处瞄一眼 🧐
模数 是一个梅森质数。
这很关键——在模质数 下,集合 对乘法构成一个循环群。
而其中,存在一些特殊的数,叫“原根”,它的一个幂次,就能跑遍整个群:
这串数,刚好是 1 到 的一个排列,周期 = 。
换句话说:
如果 A 是 的原根,且初始 UID ≠ 0,
那么 next_uid = (current × A) % p 生成的序列,会在 21 亿次内绝不重复。
而实际的玩家数?满打满算不超过 5 万。相当于你有一条绕地球 50 圈的跑道,却只打算跑 1 公里,稳得一塌糊涂~
找那个“对的人”
现在问题缩成:在 下,哪个小整数是原根?
理论上,原根存在,且数量不少(约 个),但:
- 太大的数(比如 1e9 级)乘法开销高
- 太小的数(2, 3, 5)常因阶太小被排除
- 得找一个
又小、又快、又有名有姓的
答案早已写在历史里:16807。
它等于 ,是 1988 年 Park & Miller 提出的「最小标准随机数生成器」的核心乘子 ——
一个被写进教科书、用在早期 rand() 实现中的经典选择。
验证它?只需检查对 的每个质因子 ,都满足:
这个验证在 1988 年 Park & Miller 的论文中已被严格完成;现代工具(如 Python 的 sympy)甚至毫秒内就能复现。
所以说实话,我们不必每次都亲手推一遍轮子,就像没人会在调用 rand() 前先证明一遍 LCG 的周期性:有些信任,是站在巨人肩上的效率。
好看在哪?
从 seed = 1 开始:
| step | UID | 备注 |
|---|---|---|
| 0 | 1 | 初始种子 |
| 1 | 16807 | |
| 2 | 282475249 | |
| 3 | 1622650073 | |
| 4 | 1996312012 | |
| 5 | 1033006068 | |
| … | … | |
| 2147483645 | 最后一个新值 | 下一次会回到1开始下一周期 |
没有“10001, 10002”那样的工整,也没有“1, 3, 9, 27…”那样的幂次感——
它像一条看似曲折、实则永不回头的单行道。
⚠ 声明
先说清楚:此方案安全性几乎为零,不可用于任何真实系统,现成的成熟安全方案一抓一大把。
它存在的唯一理由,是回应我当初的执念:在基岩版原版的限制下,生成一个“看起来不单调、实际上不重复”的 UID。
换句话说:
这不是一个“好”的通用 UID 方案,它只是我试了所有路之后,在原版 MC 的规则夹缝里,唯一能跑通的那条小径。
在 Minecraft 基岩版中实现
问题来了:直接套用原公式,第 4 次就会溢出成负数(实测得到 -1199696159)。
理论上可以用拆位累加法防溢出,但太麻烦了。
于是思路一转:既然用户远不到 21 亿,何必硬扛 21 亿的模数?
→ 改用更轻量的组合,上限100w:
- (大于 10⁶ 的最小质数)
- (是模 1000003 的原根,周期 = 1000002,刚好覆盖需求)
推荐把初始种子(即第一个
uid的值)设为48573——
这是2²⁰ % 1000003的结果,能让序列从“折返点”开始,避开前面那串像 2 的幂次的单调增长,观感更好
注意这只是核心生成逻辑;计分板创建、虚拟玩家(如
"2"和m)的初始化等基础步骤需自行补充。
实际只需两行命令:
scoreboard players operation uid uid *= "2" num# 将 uid 乘以 2(即 current × A)# 前提:num 计分板中,虚拟玩家 "2" 的分数为 2
scoreboard players operation uid uid %= m num# 对结果取余 1000003(即 (current × A) % M)# 前提:num 计分板中,虚拟玩家 m 的分数为 1000003
写在最后:为什么还值得折腾?
你说得对——它和「自增+映射」本质差不多。
但是,当玩家看到自己 UID 是 1033006068 而不是 5,
他第一反应不是「哦,第 5 个注册」,而是:
“这数字……怎么来的?”
“用的random?怎么避免重复的?”
那一瞬的好奇,就能让我轻哼起来~
算法可以朴素,但思考不该潦草。
在限制中跳舞,在确定中造“随机”——
这大概,是我对“好看”的执念吧
参考资料 & 延伸推荐
-
Park, S. K., & Miller, K. W. (1988).
Random Number Generators: Good Ones Are Hard to Find.
Communications of the ACM, 31(10), 1192–1201.这篇论文首次推荐了
16807作为模 的乘子,奠定了“最小标准随机数生成”的基础。 -
Park-Miller-Carta Pseudo-Random Number Generator (2005).
含原始论文、C 实现、误差分析与
48271/16807对比。 -
命令/scoreboard - 中文 Minecraft wiki
关于 Minecraft 中的计分板(scoreboard)命令的全部。
部分信息可能已经过时









