两个函数撑起的男娘指数:DJB2 哈希与 sin 伪随机
年度男娘程度总结器看起来像个测试,其实核心只有两个函数:一个把昵称变成数字,一个把数字变成 1–100 的分数。
「年度男娘程度总结器」大概是整个站里最轻松的项目:输入昵称,点一下按钮,得到 1–100 的男娘指数和一段评语。它没有后端,昵称只在本地参与计算,刷新页面结果也不会变——因为一切完全由昵称决定。🌸
先玩为敬:去测测。
需求只有一句话
最初的想法很朴素:相同的昵称必须得到相同的结果。
为什么不直接用 Math.random
如果直接用 Math.random(),同一个名字每次测出来都不一样,那这个「测试」就失去了可信度(虽然它本来也只是娱乐)。所以我需要一个确定性的映射:昵称 → 分数。哈希函数正好干这个。
第一步:DJB2 哈希
DJB2 是个很老的字符串哈希算法,核心只有一行:初始值 5381,每次把当前值乘 33 再加上字符编码。
function stringToHash(str) {
let h = 5381;
for (let i = 0; i < str.length; i++) {
h = (h << 5) + h + str.charCodeAt(i);
}
return Math.abs(h);
}
(h << 5) + h 就是 h * 33,左移比乘法写起来更有「位运算」的味道。JS 的位运算会把数字截断成 32 位整数,所以哈希值不会无限膨胀;最后取绝对值,避免出现负数。
这一步之后,任何昵称都变成了一个整数。剩下的问题是:怎么把这个可能很大的整数,压到 1–100?
第二步:sin 伪随机
常见的做法是取模,但取模容易让相邻昵称的分数也相邻,看起来不够「随机」。这里用了一个小技巧:
function seededRandom(seed) {
const x = Math.sin(seed) * 10000;
return x - Math.floor(x);
}
Math.sin 的输出在 -1 到 1 之间,乘 10000 再取小数部分,就能得到一个 [0, 1) 之间的伪随机数。它由种子唯一决定,同一个种子永远得到同一个结果,完美符合需求。
分数就是 Math.floor(seededRandom(hash) * 100) + 1,范围 1–100。不同分数对应不同评语:
| 分数 | 评价 |
|---|---|
| 90–100 | 顶级天赋型 |
| 70–89 | 资深选手 |
| 50–69 | 潜力股 |
| 30–49 | 入门级 |
| 1–29 | 直男本男 |
严格来说,sin 伪随机不是均匀分布,分数段之间的比例会有点偏差;但作为一个娱乐玩具,这点偏差完全可以接受。😌
一些界面上的小心思
- 六套配色:樱花、天空蓝、紫藤、抹茶、柚子、墨染,选完存进
localStorage; - 主题和配色在
<head>的内联脚本里就应用,避免刷新时闪一下默认色; - 输入框按回车也能开始测试,空输入会提示;
- 整个页面纯前端,昵称不会发到任何服务器,也没有埋点。
唯一需要担心隐私的地方,是别把真名输进去然后截图发群里。
当然,分数和评语都是瞎编的,图一乐就好。真要说这个项目教会了我什么,大概是:一个好玩的点子,有时候真的只需要两个函数。 ✨