全部文章
项目 约 2 分钟

两个函数撑起的男娘指数:DJB2 哈希与 sin 伪随机

年度男娘程度总结器看起来像个测试,其实核心只有两个函数:一个把昵称变成数字,一个把数字变成 1–100 的分数。

两个函数撑起的男娘指数:DJB2 哈希与 sin 伪随机

「年度男娘程度总结器」大概是整个站里最轻松的项目:输入昵称,点一下按钮,得到 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> 的内联脚本里就应用,避免刷新时闪一下默认色;
  • 输入框按回车也能开始测试,空输入会提示;
  • 整个页面纯前端,昵称不会发到任何服务器,也没有埋点。

唯一需要担心隐私的地方,是别把真名输进去然后截图发群里。

当然,分数和评语都是瞎编的,图一乐就好。真要说这个项目教会了我什么,大概是:一个好玩的点子,有时候真的只需要两个函数。 ✨

返回文章列表 RSS 订阅