游戏装备洗练概率计算器:超几何分布与纯前端实现
在《胜利女神妮姬》NIKKE这个游戏里装备养成的后期基本都耗在词条上词条随机生成洗练石产出有限锁定一个词条又要额外消耗石头。真正让人头疼的不是“哪些词条强”而是“我到底要准备多少石头才够”以及“当前这套词条应该继续洗还是先收手”。这两个问题如果只靠体感很容易出现两种结果石头全花光也没洗出目标或者洗出不错词条后又手贱洗掉。我做的这个装备洗练计算器就是用来解决这两个问题的。它把词条池数量、目标词条数、槽位数量、每次洗练消耗这些参数填进去就能算出期望需要多少石头并且在预算内给出一个可操作的建议继续洗、停手攒石头、还是先保住当前词条。整个工具用纯前端实现保存成一个 HTML 文件就能在浏览器里直接运行不需要搭建服务端也不需要安装依赖。下面先从计算器要解决的问题说起然后依次拆解概率模型、核心代码、验证方法和排查思路。1. 先理解装备洗练计算器要解决什么问题1.1 装备词条系统的核心数值先梳理游戏里和装备洗练相关的几个概念。一件装备有若干词条槽每次洗练会按照一定规则重新生成槽位上的词条消耗一定数量的洗练石如果不想让某个还不错的词条被洗掉可以用锁定功能但锁定会额外增加该轮消耗。对计算器来说真正影响概率的输入参数只有五个词条池总数、目标词条数、词条槽数、目标最少词条数量、单次洗练消耗。把这五个参数说清楚整个模型就立住了。参数含义举例词条池总数 N一次洗练时可能出现的所有词条类型数量20目标词条数 T你认为值得保留的词条类型数量5词条槽数 K一件装备最多拥有的词条槽数量4目标最少词条数 m你希望最终至少保留几个目标词条2单次洗练消耗每洗一次消耗的基础石头数10锁定单个词条消耗锁定一个目标词条后追加的石头数15这里要提醒一句不同版本、不同装备位词条池数量和词条权重都可能不同。计算器可以先把“等概率随机”作为默认模型但实际使用前要按当前游戏版本校准词条池数量。代码本身不绑定任何具体版本改参数就能适配。1.2 计算器的输入、输出和边界界面上大致需要这样一组输入字段词条池总数所有可能随机到的词条类型数量。目标词条数量玩家主观定义“好词条”的数量。词条槽数装备固定属性。当前已有目标词条数洗练前已经拥有的好词条数量。目标最少词条数洗练结束时要达到的目标。单次洗练消耗基础石头。已锁定词条数。锁定单个词条额外消耗。当前持有石头的预算上限。输出则包括三块期望洗练次数。期望石头消耗总量。在指定预算内至少成功一次的累积概率。基于预算成功率和当前进度的操作建议。这里要明确边界。计算器处理的是“词条类型”的命中概率不处理“词条数值大小”。简单说它回答的是“还要几次才能洗出至少两个目标词条”而不是“洗出来的攻击力数值是 80% 还是 95%”。数值档位属于另一层概率模型可以在后面扩展但不应该混进第一版计算器里。2. 期望石头数背后的概率模型2.1 从几何分布说起单目标词条的期望先看最简单的场景装备只有一个词条槽每次洗练等概率从 N 个词条中抽一个目标词条集合大小是 T。那么单次成功的概率是p T / N从开始洗到第一次成功需要的次数 X 服从几何分布。几何分布有一个很直接的结论E[X] 1 / p举例N 20T 5那么 p 0.25期望次数是 4 次。如果单次消耗 10 石头期望石头数就是 40。这个公式看着简单但实际使用时要特别注意一个误区期望不等于保证。几何分布的方差很大p 0.25 时中位数大约是 3均值是 4但超过 4 次才成功的概率仍然接近 32%。换句话说按期望次数准备石头大概只有三分之二的概率能成功。注意不要只按期望值准备石头。期望值是用在长期平均和预算规划上的单次洗练是否出货永远是个概率事件。更可靠的指标是“预算内累积成功率”。2.2 多槽位、保留词条和锁定机制如何影响概率单槽模型太理想实际装备往往有多个词条槽而且词条不会重复。比如词条池有 20 种词条装备有 4 个槽一次洗练会随机填上 4 个不同词条。这种情况下一次洗练里“恰好命中 t 个目标词条”的概率要用超几何分布来计算P(X t) C(T, t) * C(N - T, K - t) / C(N, K)其中 C 是组合数。上式含义是从 T 个目标词条中选出 t 个同时从 N - T 个非目标词条中选出 K - t 个所有组合数除以总组合数。“至少命中 m 个目标词条”的概率则是P(X m) sum_{tm}^{min(K,T)} C(T, t) * C(N - T, K - t) / C(N, K)期望需要的洗练次数就是 1 / P(X m)。下面用一个具体例子演示。词条池 N 20目标词条 T 5槽位 K 4。对比三种情况场景剩余槽位目标需求单次成功概率期望次数每轮消耗期望石头不锁定四槽至少两个目标42约 0.2487约 4.0210约 40.2锁定一个目标后再洗三槽31约 0.5304约 1.8925约 47.1这里有一个非常关键的数值现象锁定一个目标词条后期望次数确实从 4 次降到了 1.89 次但因为锁定费用太高总期望石头成本反而从 40.2 涨到了 47.1。所以“锁定是否划算”不是看期望次数而是看总成本。可以用临界值来辅助决策。假设基础消耗 base 10不锁定时单次成功概率 p_no_lock 0.2487锁定时单次成功概率 p_lock 0.5304锁定额外消耗为 x那么锁定更划算的条件是(1 / p_lock) * (base x) (1 / p_no_lock) * base代入数值后得到 x 约小于等于 11.3。也就是说当锁定单个词条的额外消耗超过 11 石头时在这个参数组合下反而不如直接重洗。这个结论是计算器最值得给出的判断之一。还需要注意一个模型细节锁定了一个目标词条后目标词条池也要跟着减少。比如原来 T 5锁定 1 个目标词条剩余随机抽取时目标词条只剩 4 个。如果还在用 T 5 计算概率会被明显高估。2.3 用蒙特卡洛模拟校验公式理论公式算完最好再用随机模拟做一次交叉验证。蒙特卡洛模拟的思路很简单把洗练过程用代码重复大量次数统计平均需要多少次成功然后和公式结果对账。模拟逻辑如下生成一个大小为 N 的词条池标记其中 T 个为目标词条。每次洗练从池中不放回抽取 K 个词条。判断抽取结果中是否命中至少 m 个目标词条。未命中就继续命中则记录当前次数。重复大量试验后求平均。由于这里用的是“不放回抽取”模拟模型和超几何分布保持一致。模拟次数越大平均值越接近公式结果如果模拟值和公式偏差明显说明代码里有一处参数没有对齐需要优先排查。3. 用纯前端实现一个可运行的计算器3.1 项目结构与界面布局实现选择单文件 HTML是因为这类计算器使用场景很轻要么在手机上打开要么在电脑上开着游戏窗口边看边算。单文件不需要服务器不需要打包也不需要安装 Node.js。目录结构只需要一个文件wash-calculator/ ├── index.html └── README.md界面分成三个区域顶部是参数配置表单中间是结果展示底部是蒙特卡洛模拟校验区。表单参数和计算结果之间不需要后端全部由 JavaScript 在浏览器本地计算。HTML 表单骨架可以这样写div classconfig label词条池总数 N input typenumber idpoolSize value20 min1 /label label目标词条数 T input typenumber idtargetCount value5 min1 /label label词条槽数 K input typenumber idslotCount value4 min1 /label label当前已有目标词条数 input typenumber idcurrentGood value0 min0 /label label目标最少词条数 m input typenumber idminTarget value2 min1 /label label单次洗练基础消耗 input typenumber idbaseCost value10 min1 /label label已锁定词条数 input typenumber idlockedCount value0 min0 /label label锁定单个词条额外消耗 input typenumber idlockCost value15 min0 /label label预算石头数量 input typenumber idbudget value100 min1 /label button idcalculateBtn计算期望/button /div这段表单的特点是所有参数都可以直接编辑方便针对不同装备和版本调整。value 里的默认值只是演示用实际使用时按游戏当前版本填写。3.2 核心算法代码期望、概率、模拟计算器最核心的部分是三个函数组合数、超几何概率、主流程求解。先看组合数实现function comb(n, k) { if (k 0 || k n) return 0; if (k 0 || k n) return 1; let result 1; for (let i 1; i k; i) { result result * (n - k i) / i; } return result; }这里用递推方式计算组合数避免直接做阶乘导致数字溢出。JavaScript 的 Number 类型在组合数较大时会有精度问题但词条池规模通常不超过几百这个实现足够。然后是超几何概率和至少命中 m 个目标词条的概率function hypergeometric(N, T, K, t) { return comb(T, t) * comb(N - T, K - t) / comb(N, K); } function successProbability(N, T, K, m) { if (m 0) return 1; if (K m) return 0; let sum 0; const maxT Math.min(K, T); for (let t m; t maxT; t) { sum hypergeometric(N, T, K, t); } return sum; }需要处理两个边界m 小于等于 0 时表示要求已经满足单次成功率直接是 1期望次数为 0K 小于 m 时表示槽位数量根本装不下这么多目标词条概率为 0。主流程里要把“当前已有目标词条数”和“锁定词条数”转换成模型需要的槽位和目标需求。比如当前已有 r 个目标词条并且全部锁定那真正参与随机洗练的槽位就是 K - r剩下的需求是 m - rfunction normalizeParams(input) { const remainingSlots Math.max(0, input.slotCount - input.lockedCount); const needHits Math.max(0, input.minTarget - input.currentGood); return { pool: input.poolSize, target: input.targetCount, slots: remainingSlots, minHits: needHits, perAttemptCost: input.baseCost input.lockedCount * input.lockCost, }; }这个归一化很容易错。很多人直接把 K 和 m 填进公式忘了把已经锁定并且已经达标的部分从随机范围里扣掉结果就是期望次数被高估或低估。锁定词条在模型里等价于“移除一个槽位同时减少一个需求”。最终期望石头数的计算函数是function expectedStones(params) { const p successProbability(params.pool, params.target, params.slots, params.minHits); if (p 0) return Infinity; const expectedAttempts 1 / p; return { probability: p, expectedAttempts: expectedAttempts, expectedStones: expectedAttempts * params.perAttemptCost, }; }预算内成功率也要单独算。这里不是看期望而是看在预算允许的尝试次数内至少成功一次的概率function probabilityWithinBudget(p, budget, perAttemptCost) { if (p 0 || p 1) return 0; const allowedAttempts Math.floor(budget / perAttemptCost); if (allowedAttempts 0) return 0; return 1 - Math.pow(1 - p, allowedAttempts); }最后是蒙特卡洛模拟函数。这里用一套随机抽样逻辑模拟整个洗练过程用来和服务度公式对账function sampleWithoutReplace(pool, size) { const source pool.slice(); const result []; for (let i 0; i size; i) { const index Math.floor(Math.random() * source.length); result.push(source.splice(index, 1)[0]); } return result; } function monteCarlo(poolSize, targetSize, slots, minHits, trials, limit) { const pool Array.from({ length: poolSize }, (_, i) i); const targets new Set(Array.from({ length: targetSize }, (_, i) i)); let totalAttempts 0; let successCount 0; for (let trial 0; trial trials; trial) { for (let attempt 1; attempt limit; attempt) { const chosen sampleWithoutReplace(pool, slots); let hits 0; for (const c of chosen) { if (targets.has(c)) hits; } if (hits minHits) { totalAttempts attempt; successCount; break; } } } return successCount 0 ? totalAttempts / successCount : Infinity; }模拟上限要设置一个比较大的值比如 100000防止在概率极低时无限循环。如果成功率本身就接近 0模拟结果会出现 Infinity这也是一个检查输入参数是否合理的信号。3.3 生成下一步操作建议的决策逻辑算出数字还不够计算器要直接给出建议。建议逻辑可以分成两段先判断预算内成功率再判断当前是否已经达到目标。决策规则使用一张对照表预算内成功率建议大于等于 80%当前预算内成功率较高可以正常继续洗练50% 到 80%可以洗但必须设置止损线不要超预算硬洗小于 50%建议停手攒石头或者降低目标词条数量建议生成的 JavaScript 可以写成这样function buildAdvice(result, params) { const lines []; const rate result.probInBudget; if (rate 0.8) { lines.push(当前预算内成功率较高可以按计划继续洗练。); } else if (rate 0.5) { lines.push(预算内成功率中等建议设置止损线例如只消耗预算的一半。); } else { lines.push(预算内成功率较低建议先攒石头或者降低目标词条数量。); } if (params.currentGood params.minTarget) { lines.push(当前已经达到目标词条数量继续洗练的收益主要来自词条数值提升需要自行权衡风险。); } return lines; }这里没有把“期望石头数”作为唯一决策依据因为期望值只代表长期平均。对一个石头存量有限的玩家来说预算内成功率比期望值更能反映“我现在该不该赌这一把”。在连续多次没出货时要防止增加预算的冲动。建议在洗练前就把预算上限写好。连续失败时最容易上头计算器显示成功率没有明显变化但预算一旦被打破损失就不是概率问题而是资源管理问题了。4. 运行验证如何确认计算结果可靠4.1 用固定参数手工验证计算器做完后第一件事不是填真实游戏数据而是先用一组手算结果做对照。取一组能手工推导的参数N 20T 5K 4m 2基础消耗 10。组合数手算如下C(20, 4) 4845C(5, 2) * C(15, 2) 1050C(5, 3) * C(15, 1) 150C(5, 4) * C(15, 0) 5至少命中 2 个目标词条的概率 (1050 150 5) / 4845 ≈ 0.2487。期望次数 1 / 0.2487 ≈ 4.02。期望石头 40.2。如果界面上填入这组参数后得到同样的结果说明核心公式实现没有大问题。4.2 模拟结果与理论期望的对账再用蒙特卡洛模拟验证。把参数 N 20T 5K 4m 2 传入 monteCarlo 函数模拟 10 万次结果应该在 4.0 到 4.1 之间浮动。如果模拟均值明显偏离公式值比如超过 4.3就要检查抽样过程是不是用了有放回抽取或者随机池生成有误。一组可能的输出对比验证方式期望次数超几何公式计算4.02蒙特卡洛模拟 10 万次4.03 左右两者不一致的原因有两个一是随机抽样本身有误差试验次数越多越接近理论值二是代码模型和公式模型不对齐比如抽样方式、目标池更新逻辑不同。排错时优先对比后者。4.3 边界条件和异常输入边界条件最容易暴露代码问题。至少要测下面几组输入当前已有目标词条数等于目标最少词条数时期望次数应为 0。词条槽数小于目标最少词条数时概率应为 0。已锁定词条数大于总槽数时应该被归一化到 0 或提示输入非法。预算小于单次消耗时预算内成功率应为 0。词条池总数小于目标词条数时属于非法输入应提示用户。输入参数前先做边界校验否则计算器和浏览器都会给出误导结果。期望也是如果直接得到 Infinity 或 NaN优先检查输入组合是否满足基本约束。实现时可以在 calculate 函数开头加一段校验逻辑把所有参数转成数字检查是否为正数再进入计算。这样能避免很多低级错误。5. 常见问题与排查思路5.1 计算结果比实际体感低很多现象公式算出来 40 石头就能达到目标实际花了 80 石头还没出。可能原因有两个。第一词条池中不同词条的权重不是均匀的比如攻击类词条的出现概率明显低于防御类而公式默认所有词条等概率。第二期望值本身不等于“你一定会在这个次数内成功”单次洗练的尾部概率很大。排查步骤先确认目标词条数 T 是否合理。再看游戏内词条是否独立概率如果是权重模型需要把均匀超几何模型替换为加权抽样模型。最后还是建议看预算内成功率不要只看期望值。处理建议如果游戏内确实存在词条权重差异不能直接套用本文的等概率模型。需要先用大量洗练样本统计词条出现频率把权重数组引入模拟代码。5.2 词条权重和默认概率不一致现象界面算出的单次成功概率是 0.25但体感远低于这个值。原因均匀概率只是一个简化假设。游戏可能对部分词条做了稀有度倾斜导致目标词条的实际权重更低。检查方式用模拟统计词条出现频率。如果某个目标词条 1000 次里只出现 50 次而均匀概率应该是 50 次以上说明这个词条被降权了。处理建议把模型从“目标词条集合大小”升级为“每个词条的权重数组”。单次命中概率不再是 T / N而是目标词条权重之和除以所有词条权重之和。多槽位情况也需要改为按权重不放回抽样。5.3 浏览器打开页面后按钮无响应现象双击打开 HTML 文件点击“计算期望”没有任何反应。排查