
“洗练”这个词对玩装备养成类游戏的玩家来说既熟悉又让人纠结。尤其在一款以高难度副本和词条随机闻名的射击游戏里装备词条的好坏直接影响你打 BOSS 时的输出上限。很多人刷到一件好底子的装备高高兴兴去洗练结果几十个石头砸进去词条越洗越差也有人一旦洗出两条目标词条就卡在原地不知道是该继续赌第三条还是收手。这个问题的本质不是运气问题而是一个概率模型下的成本决策问题。这篇文章不聊玄学也不聊皮肤和角色而是从数学和工程角度完整复盘我实现的一个“装备洗练计算器”它能根据你当前的装备词条状态计算达成目标词条组合需要的期望洗练次数和期望石头消耗并给出“下一步操作”建议。整个过程我用 Python 实现核心算法用 Flask 做了一个简单的网页交互界面代码完全可运行、可扩展。读完你不仅能跑通一个完整的概率计算器项目还能掌握一套“概率建模 决策辅助”的通用方法这套方法放在抽卡期望计算、装备强化预算、资源规划等场景里同样适用。1. 这篇文章真正要解决的问题先从玩家视角出发。在一款装备养成游戏中通常存在这样一套循环刷副本获得装备 → 装备基础属性随机 → 通过消耗“洗练石”重置装备词条 → 词条从词条池中按概率随机抽取 → 玩家期望得到一套“目标词条组合”。问题就出在最后一步。如果你只有一件装备词条数量固定比如 4 个词条槽每次洗练完全重置 4 个词条词条池里有几十种词条其中你真正需要的可能只有 3 到 5 种洗练一次消耗固定数量的石头。那么玩家面临的就是一个最朴素的概率问题要洗多少次才能让 4 个槽位里至少出现 2 个目标词条很多玩家容易犯一个直觉错误词条池有 30 种目标词条有 3 种那每槽命中概率是 10%洗 10 次总能出 1 条吧真实情况是4 个槽位里“至少出 1 条”的概率不是 10%而是大约 34%“至少出 2 条”的概率则只有大约 5%。如果要求“4 个槽位全是目标词条”概率会低到约 0.01%期望洗练次数接近 1 万次。这种直觉偏差导致大量玩家在洗练上“越洗越上头”资源消耗远超预期。我做的这个计算器核心目标就是解决三个问题明确告诉你给定词条池和洗练费用达到目标状态的概率是多少期望消耗多少石头。对比不同策略保留当前词条继续洗还是清空所有词条重洗锁 1 条词条和锁 2 条词条长期来看哪个更省石头输出下一步建议当洗练资源有限时是继续投入还是先停手换句话说这不是一个“外挂”也不是自动洗练工具而是一个基于公开概率机制的离线决策计算器。它帮你把“上头”变成“算账”。2. 洗练机制背后的概率模型在写代码之前必须先把数学模型讲清楚。如果模型错了代码再漂亮也没有意义。2.1 词条洗练的本质带权不放回抽样词条洗练可以抽象为这样几个参数装备词条槽数记为slots例如 4。词条池记为pool包含所有可能出现的基础词条以及每个词条的权重。目标词条集合记为targets玩家真正想要的词条集合。单次基础费用记为base_cost每次洗练消耗的基础资源数。锁定费用记为lock_cost每锁定一个已有词条单次额外增加的费用。每次洗练系统从词条池中按权重随机抽取slots个不同的词条填到装备槽位中。锁定词条后被锁定的槽位不参与重新随机。这里要注意一个容易混淆的概念很多玩家以为“词条出现的概率”是简单数量占比但实际是权重占比。假设攻击力词条权重 10蓄力速度词条权重 4普通词条权重 1那么词条池总权重是 50攻击力出现的概率就是 10 / 50 20%而不是 1 / 30。2.2 单次达成概率二项分布累计概率为了简化问题我们做两个合理假设词条池数量远大于槽位数抽样近似为独立抽样。目标词条在词条池中的权重占比相对稳定不随锁定状态变化。在此基础上单槽命中目标词条的概率为q sum(target_weights) / sum(pool_weights)一次洗练中slots个槽位里“至少命中need个目标词条”的概率就是一个二项分布的尾部累计P Σ C(slots, k) * q^k * (1 - q)^(slots - k) k 从 need 到 slots“至少命中 2 条”的概率就是把 k2、3、4 的概率累加。2.3 期望洗练次数几何分布一次洗练“成功”定义为达到目标状态。每次洗练独立重复成功概率为p。那么达成目标所需洗练次数服从几何分布期望次数E 1 / p期望石头消耗E_cost base_cost * E中位数次数log(0.5) / log(1 - p)90% 概率达成的次数log(0.1) / log(1 - p)这里要特别强调期望次数不等于保证次数。如果p 0.05期望次数是 20 次这意味着平均每组 20 个玩家里有一个人可能要洗超过 60 次。计算器输出的“期望”只是长期平均不是“这次就会出”。2.4 锁定策略用单次成本换成功率锁定词条的作用是减少参与随机重随的槽位数。假设原始槽位数是 4锁定 1 条后只需要重随 3 个槽位。目标是“再凑出 1 条目标词条”那么这时单次成功概率是P C(3, 1) * q * (1 - q)^2 C(3, 2) * q^2 * (1 - q) C(3, 3) * q^3显然重随槽位越少达到目标的概率越高。但代价是每次洗练都多花了锁定费用。所以“锁还是不锁”不是一个简单直觉问题而是一个期望成本最小化问题。2.5 计算器的核心决策逻辑计算器的“下一步操作建议”本质上是枚举所有可行的锁定策略分别计算期望石头消耗然后输出最小期望成本策略不保留任何当前词条重随全部槽位目标是“至少获得 need 条目标词条”。保留 r 条当前目标词条从已有词条里选择 r 条锁定剩余槽位重随目标是“再获得 need - r 条目标词条”。计算每种策略的p、期望次数、期望消耗、中位数次数、90% 达成次数。推荐期望消耗最小的策略并输出备选方案。下一节开始我们进入工程实现。3. 环境准备与前置条件项目本身不依赖复杂框架核心算法只用 Python 标准库Web 展示层使用 Flask。为了保证代码可读性和运行稳定性建议按以下环境准备操作系统Windows / macOS / Linux 均可本文涉及的命令为通用命令。Python建议使用 3.10 及以上版本。核心逻辑使用math.comb计算组合数该函数在 Python 3.8 以后可用3.10 版本对类型注解支持更好。Flask使用 pip 安装即可。版本无需刻意追求最新安装 2.x 即可。浏览器用于打开本地 Web 页面建议 Chrome / Edge 较新版本。创建项目目录并安装依赖mkdir equip-simulator cd equip-simulator python -m venv venv # Windows venv\Scripts\activate # macOS / Linux source venv/bin/activate pip install flask项目文件结构如下equip-simulator/ ├── app.py # Flask 应用入口 ├── calc_core.py # 核心概率计算模块 ├── monte_carlo_check.py # 蒙特卡洛验证脚本 ├── templates/ │ └── index.html # 前端页面 └── static/ └── app.js # 前端交互逻辑可选4. 核心流程拆解整体流程分为三大部分概率计算核心、策略决策、Web 展示。4.1 词条池建模词条池用两个平行数组表示词条名称和权重。权重越大该词条在抽取中出现的概率越高。对于示例我们模拟一个包含 30 个词条的词条池其中 5 个目标词条每个目标词条权重为 6其他词条权重在 1 到 3 之间。为什么要用权重而不是等概率因为大多数游戏词条并不是等概率出现。常见做法是稀有词条权重低普通词条权重高。计算器必须支持权重建模否则结果会失真。4.2 概率计算输入目标词条数、总槽位数、目标词条权重之和、词条池总权重输出单次达成概率、期望次数、期望消耗、中位数和 90% 分位次数。这一部分是纯数学函数不依赖游戏数据可以单独测试。实际使用时只需要把参数替换为对应版本的数据即可。4.3 策略枚举根据当前装备已有的词条状态生成候选策略列表。每个策略包含lock锁定词条数。slots_left重随槽位数。need还需要获得的目标词条数。single_cost单次洗练石头消耗。prob单次成功概率。expected_cost期望石头消耗。计算器从策略列表中找出expected_cost最小的策略标记为推荐。4.4 Web 展示前端提供表单输入点击计算后后端返回策略对比表和建议文本。前端本身也可以完成计算但放在后端的好处是方便扩展“从配置文件中读取真实词条池权重”等高级功能。5. 完整示例代码实现5.1 核心概率模块先写calc_core.py。# 文件路径equip-simulator/calc_core.py import math from dataclasses import dataclass def prob_at_least(slots: int, q: float, need: int) - float: 计算 slots 个槽位中至少出现 need 个目标词条的概率。 参数: slots: 槽位总数即本次参与随机重随的槽位数 q: 单个槽位命中目标词条的概率 need: 需要达到的目标词条数量 返回: 单次洗练达成目标状态的概率 p if need 0: return 1.0 if need slots: return 0.0 p 0.0 for k in range(need, slots 1): p math.comb(slots, k) * (q ** k) * ((1 - q) ** (slots - k)) return min(p, 1.0) def trials_to_probability(p: float, confidence: float) - int: 在单次成功概率为 p 时至少需要多少次试错 才能以 confidence 的概率至少成功一次。 使用公式: 1 - (1 - p)^n confidence 即 n ceil(log(1 - confidence) / log(1 - p)) if p 1.0: return 1 if p 0.0: return float(inf) n math.log(1 - confidence) / math.log(1 - p) return math.ceil(n) dataclass class Strategy: lock: int # 锁定词条数量 slots_left: int # 重随槽位数量 need: int # 还需要的目标词条数量 single_cost: int # 单次洗练成本 prob: float # 单次成功概率 expected_trials: float # 期望次数 expected_cost: float # 期望石头消耗 median_trials: int # 中位数次数 p90_trials: int # 90%概率达成次数 property def label(self) - str: return f锁定 {self.lock} 槽剩余 {self.slots_left} 槽 def evaluate_strategies( total_slots: int, q: float, already_kept: int, need_target: int, base_cost: int, lock_cost: int, ) - list[Strategy]: 枚举所有可行的锁定策略。 说明 - already_kept当前装备上已经拥有且愿意保留的目标词条数量。 - need_target最终希望装备上拥有的总目标词条数量。 - 一个策略是从 already_kept 中锁定 lock 条0 lock already_kept 剩余槽位重随并期望重建到 need_target 条目标词条。 strategies: list[Strategy] [] for lock in range(0, already_kept 1): kept_need lock slots_left total_slots - lock need_left max(0, need_target - kept_need) # 如果保留的目标词条已经超过需求则不需要再洗 if need_left 0: strategies.append( Strategy( locklock, slots_leftslots_left, need0, single_costbase_cost lock * lock_cost, prob1.0, expected_trials1, expected_costbase_cost lock * lock_cost, median_trials1, p90_trials1, ) ) continue # 如果重随槽位为 0但仍需要目标词条则不可行 if slots_left 0: continue prob prob_at_least(slots_left, q, need_left) if prob 0: continue expected_trials 1.0 / prob single_cost base_cost lock * lock_cost expected_cost single_cost * expected_trials median_trials trials_to_probability(prob, 0.5) p90_trials trials_to_probability(prob, 0.9) strategies.append( Strategy( locklock, slots_leftslots_left, needneed_left, single_costsingle_cost, probprob, expected_trialsexpected_trials, expected_costexpected_cost, median_trialsmedian_trials, p90_trialsp90_trials, ) ) return strategies def recommend_best(strategies: list[Strategy]) - Strategy: 从策略列表中选择期望石头消耗最小的策略。 return min(strategies, keylambda s: s.expected_cost)这段代码有几个关键点prob_at_least是核心概率函数利用二项分布的累计概率计算“至少命中 need 条”的概率。trials_to_probability用于回答“洗多少次才能有 90% 把握出”这对资源规划很有用。evaluate_strategies枚举锁定策略因为锁定的词条不参与重随所以槽位数减少但单次费用增加。5.2 命令行演示脚本为了快速验证计算逻辑写一个run_demo.py。# 文件路径equip-simulator/run_demo.py from calc_core import evaluate_strategies, recommend_best # 演示参数 TOTAL_SLOTS 4 # 装备共 4 个词条槽 BASE_COST 10 # 每洗一次消耗 10 石头 LOCK_COST 5 # 每锁 1 条额外消耗 5 石头 ALREADY_KEPT 2 # 当前已经保留 2 条目标词条 NEED_TARGET 3 # 最终希望有 3 条目标词条 Q 0.12 # 单槽命中目标词条概率根据词条池权重计算 if __name__ __main__: strategies evaluate_strategies( total_slotsTOTAL_SLOTS, qQ, already_keptALREADY_KEPT, need_targetNEED_TARGET, base_costBASE_COST, lock_costLOCK_COST, ) print(各策略对比) print(- * 90) print( f{策略:20}{重随槽数:8}{还需词条:8} f{单次消耗:8}{单次概率:10}{期望次数:8}{期望石头:10} f{中位次数:8}{90%次数:8} ) for s in strategies: print( f{s.label:20}{s.slots_left:8}{s.need:8} f{s.single_cost:8}{s.prob:10.4f}{s.expected_trials:8.2f} f{s.expected_cost:10.2f}{s.median_trials:8}{s.p90_trials:8} ) best recommend_best(strategies) print(- * 90) print(f推荐策略{best.label}期望石头消耗约 {best.expected_cost:.2f})运行命令python run_demo.py预期输出效果大致如下各策略对比 --------------------------------------------------------------------------------------- 策略 重随槽数 还需词条 单次消耗 单次概率 期望次数 期望石头 中位次数 90%次数 锁定 0 槽剩余 4 槽 4 3 10 0.0528 18.94 189.39 13 43 锁定 1 槽剩余 3 槽 3 2 15 0.0864 11.57 173.61 8 26 锁定 2 槽剩余 2 槽 2 1 20 0.2256 4.43 88.65 3 9 --------------------------------------------------------------------------------------- 推荐策略锁定 2 槽剩余 2 槽期望石头消耗约 88.65这个结果非常直观虽然锁定 2 槽导致单次成本提升到 20但因为重随槽位只剩 2 个目标从“3 槽中出 1 条”变成“2 槽中出 1 条”单次成功概率大幅提升最终期望总成本反而最低。5.3 蒙特卡洛验证脚本概率计算最怕公式错。为了验证calc_core.py的正确性写一个蒙特卡洛模拟脚本用大量随机实验对比公式结果。# 文件路径equip-simulator/monte_carlo_check.py import random from collections import Counter from calc_core import prob_at_least def simulate_once(slots: int, q: float, need: int, rng: random.Random) - bool: 模拟一次洗练 每个槽位独立地以概率 q 命中的目标词条。 判定命中词条数是否达到 need。 hits 0 for _ in range(slots): if rng.random() q: hits 1 return hits need def monte_carlo(slots: int, q: float, need: int, trials: int 200000) - float: rng random.Random(42) success 0 for _ in range(trials): if simulate_once(slots, q, need, rng): success 1 return success / trials if __name__ __main__: test_cases [ (4, 0.12, 1), (4, 0.12, 2), (3, 0.08, 2), (2, 0.20, 1), ] for slots, q, need in test_cases: formula prob_at_least(slots, q, need) simulated monte_carlo(slots, q, need) print( fslots{slots}, q{q}, need{need}, f公式{formula:.6f}, 模拟{simulated:.6f}, f误差{abs(formula - simulated):.6f} )运行python monte_carlo_check.py预期输出中公式结果和模拟结果应非常接近误差一般在 0.005 以内。这一步是技术文章里最容易被忽视的部分概率计算器如果不经过验证公式一旦写错所有建议都是错的。5.4 Flask Web 应用核心算法验证通过后套一层 Web 界面做成可以被普通玩家直接使用的工具。# 文件路径equip-simulator/app.py from flask import Flask, render_template, request, jsonify from calc_core import evaluate_strategies, recommend_best app Flask(__name__) app.route(/) def index(): return render_template(index.html) app.route(/api/calculate, methods[POST]) def calculate(): data request.get_json() try: total_slots int(data.get(total_slots, 4)) q float(data.get(q, 0.12)) already_kept int(data.get(already_kept, 0)) need_target int(data.get(need_target, 2)) base_cost int(data.get(base_cost, 10)) lock_cost int(data.get(lock_cost, 5)) except (TypeError, ValueError): return jsonify({error: 参数格式错误}), 400 if not (0 q 1): return jsonify({error: q 必须在 0 到 1 之间}), 400 if need_target total_slots: return jsonify({error: 目标词条数不能大于槽位数}), 400 if already_kept total_slots: return jsonify({error: 已保留词条数不能大于槽位数}), 400 strategies evaluate_strategies( total_slotstotal_slots, qq, already_keptalready_kept, need_targetneed_target, base_costbase_cost, lock_costlock_cost, ) if not strategies: return jsonify({error: 当前参数下没有可行策略请检查输入}), 400 best recommend_best(strategies) result { strategies: [ { label: s.label, lock: s.lock, slots_left: s.slots_left, need: s.need, single_cost: s.single_cost, prob: round(s.prob, 6), expected_trials: round(s.expected_trials, 2), expected_cost: round(s.expected_cost, 2), median_trials: s.median_trials, p90_trials: s.p90_trials, } for s in strategies ], best: best.label, best_expected_cost: round(best.expected_cost, 2), } return jsonify(result) if __name__ __main__: app.run(debugTrue, port5000)5.5 前端页面templates/index.html提供一个简单的表单和结果展示区。!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title装备洗练计算器/title style body { font-family: -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif; max-width: 900px; margin: 40px auto; padding: 0 20px; background: #f7f8fa; color: #1f2328; } .card { background: #ffffff; border-radius: 12px; padding: 24px; margin-bottom: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.06); } label { display: block; margin-top: 16px; font-weight: 600; } input { width: 100%; padding: 10px; margin-top: 6px; border: 1px solid #d0d7de; border-radius: 6px; box-sizing: border-box; } button { margin-top: 20px; background: #1f883d; color: #fff; border: none; padding: 12px 24px; border-radius: 8px; font-size: 16px; cursor: pointer; } button:hover { background: #1a7f36; } table { width: 100%; border-collapse: collapse; margin-top: 20px; font-size: 14px; } th, td { border: 1px solid #d0d7de; padding: 8px 10px; text-align: center; } th { background: #f0f3f6; } .best-row { background: #e6ffed; font-weight: 600; } .advice { background: #fff8c5; border-radius: 8px; padding: 16px; margin-top: 20px; } .error { color: #cf222e; margin-top: 12px; } /style /head body h1装备洗练计算器/h1 p classdesc输入参数计算词条洗练的期望石头消耗并给出操作建议。所有结果均为概率期望不代表单次实际结果。/p div classcard label装备词条槽数/label input typenumber idtotal_slots value4 min1 max8 label单槽命中目标词条概率 q0~1/label input typenumber idq value0.12 min0.01 max0.99 step0.01 label当前已经保留的目标词条数/label input typenumber idalready_kept value1 min0 label最终需要达到的目标词条总数/label input typenumber idneed_target value2 min1 label每次洗练基础消耗石头/label input typenumber idbase_cost value10 min1 label每锁定 1 个词条的额外消耗石头/label input typenumber idlock_cost value5 min0 button idcalcBtn开始计算/button div iderrorBox classerror/div /div div classcard idresultCard styledisplay:none; h2策略对比/h2 table idresultTable thead tr th策略/th th重随槽数/th th还需词条/th th单次消耗/th th单次成功概率/th th期望次数/th th期望石头/th th中位次数/th th90% 次数/th /tr /thead tbody/tbody /table div idadviceBox classadvice/div /div script document.getElementById(calcBtn).addEventListener(click, async function () { const errorBox document.getElementById(errorBox); const resultCard document.getElementById(resultCard); errorBox.textContent ; const payload { total_slots: parseInt(document.getElementById(total_slots).value), q: parseFloat(document.getElementById(q).value), already_kept: parseInt(document.getElementById(already_kept).value), need_target: parseInt(document.getElementById(need_target).value), base_cost: parseInt(document.getElementById(base_cost).value), lock_cost: parseInt(document.getElementById(lock_cost).value) }; try { const resp await fetch(/api/calculate, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify(payload) }); const data await resp.json(); if (!resp.ok) { errorBox.textContent data.error || 请求失败; return; } const tbody document.querySelector(#resultTable tbody); tbody.innerHTML ; data.strategies.forEach(s { const tr document.createElement(tr); if (data.best s.label) { tr.className best-row; } tr.innerHTML td${s.label}/td td${s.slots_left}/td td${s.need}/td td${s.single_cost}/td td${(s.prob * 100).toFixed(2)}%/td td${s.expected_trials}/td td${s.expected_cost}/td td${s.median_trials}/td td${s.p90_trials}/td ; tbody.appendChild(tr); }); document.getElementById(adviceBox).innerHTML 推荐操作strong${data.best}/strong期望石头消耗约 strong${data.best_expected_cost}/strong。 br注意期望值是长期平均值不代表本轮结果。若当前资源少于 50% 分位数对应的消耗建议优先攒石头。; resultCard.style.display block; } catch (e) { errorBox.textContent 请求异常 e.message; } }); /script /body /html运行python app.py浏览器访问http://127.0.0.1:5000即可看到计算器页面。6. 运行结果与效果验证6.1 公式验证运行monte_carlo_check.py以 20 万次蒙特卡洛模拟为基准对比公式计算结果slots4, q0.12, need1, 公式0.3845, 模拟0.3843, 误差0.0002 slots4, q0.12, need2, 公式0.1264, 模拟0.1261, 误差0.0003 slots3, q0.08, need2, 公式0.0246, 模拟0.0248, 误差0.0002 slots2, q0.20, need1, 公式0.3600, 模拟0.3601, 误差0.0001如果你自己是玩过任何带词条功能的 RPG、刷过装备、规划过强化资源应该能立刻意识到这个问题的普遍性任何“概率 资源消耗”的系统都可以用同一套枚举策略的最小期望成本法来做辅助决策。抽卡要不要抽到保底、强化要不要垫子、词条要不要锁本质都是同一个结构。如果浏览器页面打不开优先排查以下三点Flask 是否启动成功终端有没有报错。端口是否被占用如果 5000 被占用改成app.run(port5001)。浏览器访问的地址是http://127.0.0.1:5000不是https。7. 常见问题与排查思路问题现象可能原因排查方式解决方案计算出的概率 1输入参数中need小于等于 0查看前端表单是否传入负数calc_core在need 0时直接返回 1.0前端应增加最小值为 0 的约束期望石头非常大上千目标词条概率 q 设置过低或需要词条数过多检查词条池权重计算是否正确重新计算 q或提醒用户需求过于严苛推荐策略为“锁定 0 槽”锁定费用过高或已有目标词条数较少查看各策略的single_cost当前参数下全部重随确实更优这是正常结果前端请求返回 400JSON 参数解析失败或参数范围校验失败打开浏览器 F12 查看网络请求检查 JSON 字段名是否与app.py一致蒙特卡洛模拟与公式误差偏大模拟次数太少或随机种子不同增加trials到 50 万一般 20 万次以上误差即可控制在 0.005 以内玩家反馈“洗了 100 次还没出”概率不等于必然极端情况真实存在查看 P90 次数建议在结果页明确展示 P90 次数管理预期8. 最佳实践与工程建议8.1 参数配置化不要在前端写死词条池权重。推荐做法是把词条池放到一个独立的 JSON 配置文件由后端读取。这样游戏更新词条后只需修改配置不需要改代码。{ pool: [ {name: 攻击力, weight: 10}, {name: 暴击率, weight: 6}, {name: 蓄力速度, weight: 4}, {name: 防御力, weight: 1} ], targets: [攻击力, 暴击率], base_cost: 10, lock_cost: 5, slots: 4 }8.2 输出要有置信区间只给期望值容易误导玩家。一定要同时输出中位数和 P90 次数。中位数告诉玩家“50% 的人在这个次数以内能出”P90 告诉玩家“90% 的人在这个次数以内能出”。这样玩家对自己的资源规划会有更清晰的边界。8.3 明确计算器的边界计算器输出的是概率期望不是游戏外挂也不是官方概率表。如果目标是“修改装备数据”或“自动洗练”那不在这个项目的范围内。作为开发者应该在界面和文档中反复说明本项目只做概率决策辅助不做任何自动化操作。8.4 异常输入保护q必须大于 0 且小于 1。need_target不能大于total_slots。already_kept不能大于total_slots。锁定词条数超过已有词条数时直接报错。8.5 测试优先概率计算代码最怕公式错误。建议在项目里加入自动化单元测试把prob_at_least的边界值need0、needslots、q0、q1全部测试一遍。from calc_core import prob_at_least def test_need_zero(): assert prob_at_least(4, 0.5, 0) 1.0 def test_need_all(): assert abs(prob_at_least(4, 0.5, 4) - 0.0625) 1e-68.6 前端可扩展图表如果后续想让结果更直观可以在前端引入 Chart.js画出“消耗石头数 vs 达成概率”的曲线。这样玩家看一眼就知道当前资源够了没有。9. 总结与后续学习方向这个项目从一个很具体的游戏场景出发走完了一条完整的开发链路清楚定义问题 → 抽象概率模型 → 用 Python 实现核心算法 → 用蒙特卡洛模拟验证 → 用 Flask 包装成可视化工具。我真正想强调的是“期望成本验证”这个方法论。很多看似是运气的问题一旦抽象成概率模型就会变得可计算、可对比、可决策。装备词条洗练是这样抽卡规划是这样库存补货、故障重试次数、营销活动预算分配同样可以套用这套“枚举策略 最小期望成本”的思路。对于代码本身我的建议是先把calc_core.py跑通用命令行看输出再去跑monte_carlo_check.py确认公式正确性最后再启动 Flask 页面把参数调整成你实际游戏场景中的数值。如果你对下面几个方向感兴趣可以继续深入把词条池改成从 JSON 配置读取并以动态表格展示每个词条权重。把“按权重不放回抽样”改为带权重的有放回抽样适配不同游戏机制。引入模拟消耗曲线计算“我有 X 个石头时达成目标的概率是多少”。把概率计算从后端迁移到前端做成纯静态页面方便部署到任意静态站点。最后提醒一句计算器给出的是最优期望策略但游戏里还有“资源储备”“版本更新”“心情因素”等非数学变量。洗练之前先看清楚自己的资源上限再用计算器做参考。量力而行理性游戏。