简介:《原神抽卡模拟器.zip》面向想用 C++ 复刻游戏祈愿流程的学习者与爱好者,围绕角色与武器抽取的概率分布、保底计数和结果统计,搭建可运行的小型项目,也适合作为面向对象编程、随机数应用与简易登录校验的练习素材。压缩包共 38 个文件,约 4.04MB,以 cpp/hpp 源码、txt 数据与日志、tlog/pdb/obj 等编译中间文件、exe 可执行程序及 sln/vcxproj 工程文件为主,兼顾源码阅读、直接运行和 Visual Studio 二次开发。已有 1606 人学习下载。读者可从中了解祈愿池数据结构、随机抽取与保底规则如何落到代码,登录与 MD5 校验、用户记录和抽卡统计如何组织,并借助 README 与工程配置快速梳理目录、复现实验环境,适合作为课程设计或 C++ 小项目参考。
1. 一个能跑起来的抽卡概率实验台
抽卡这件事,玄学归玄学,代码不会骗人。我拿到的这个压缩包,标题叫「原神抽卡模拟器.zip」,核心是一门 C++ 写的抽卡概率仿真程序——它把「保底」「大保底」「软保底」这些玩家嘴里说不清、策划也不细讲的规则,用代码和计数器还原成一串能打印出来的数字。你手里有 C++ 环境就能编译,跑几万次十连,看概率分布是不是和你体感的“非”对得上。适合谁?想拿抽卡概率做算法练手的在校同学、想验证公开概率模型合理性的从业者,以及单纯想搞清楚自己到底被第几发保底救了的人。它不是外挂、不碰游戏客户端,只做纯离线蒙特卡洛模拟,用 C++ 写成,共用结构体就够用,不依赖图形库。
2. 拆开源码:一次抽卡在 C++ 里是怎么被算出来的
先别急着编译。这个模拟器值不值得用,取决于你对「抽卡规则怎么变成代码」这件事有没有底。我一般会先把压缩包里的核心计算路径顺着读一遍,再动手改参数。读懂了,后面改概率、加武器池、跑几十万次统计,全都心里有数。
2.1 五类抽卡规则的建模思路
抽卡模拟的关键在于把游戏规则抽象成状态机,而不是随机数本身。C++ 这边最自然的写法是一个GachaState结构体,里面放三个计数器:pity(当前距离保底的抽取数)、guarantee(是否大保底状态)、totalCount(总抽数)。每一次单抽,先判断pity到没到上限,再决定用基础概率还是软保底概率,最后才生成随机数。
这套规则通常被拆成几层:
| 规则层 | 状态字段 | 作用 |
|---|---|---|
| 硬保底 | pity >= 89 | 第 90 抽强制出金 |
| 软保底 | pity >= 73 | 从第 74 抽起概率逐抽递增 |
| 大保底 | guarantee | 上次歪了,本次必出当期 |
| 小保底 | !guarantee | 50% 概率出当期或常驻 |
| 保底重置 | 出金后清零 | 出金即pity = 0 |
严格说,官方只公开了“综合概率约 1.6%”和“90 抽必出”两条信息,软保底的递增值是社区通过海量样本反推出来的。常见做法是把第 74 抽到第 89 抽的概率按等差或等比往上叠,叠加曲线不同,最终的金色期望抽数会差 1 到 3 抽。这个模拟器就是让你把这条曲线做成可调参数,跑出分布看看哪种递推和公开数据的“平均 62.5 抽出金”更接近。
这也是我用它做实验的主要原因:不改参数,它只是一个演示程序;改了参数,它就是一个概率分布的验证工具。C++ 在这里的优势不是唯一,但很实际——抽卡模拟要做几十万甚至上千万次采样,C++ 跑一遍几秒钟,换脚本语言可能要多等一个数量级的时间,调参时这个差别很明显。
2.2 编译与第一次运行
压缩包里通常是一个main.cpp加若干头文件,纯标准库实现。我习惯单独建一个构建目录,避免中间产物污染源码目录:
# 解压后进入源码目录,先确认文件结构 cd gacha-sim && ls -la # 用 C++17 标准编译,开启二级优化 g++ -std=c++17 -O2 -Wall -Wextra -o gacha main.cpp gacha.cpp # 先跑一次小样本,确认逻辑正常 ./gacha --pulls 1000 --seed 42参数含义需要说清楚,不然跑出来的数你会误读:
--pulls:总抽数,建议先用 1000 试跑,看输出格式是否符合预期,再上到1000000。--seed:随机种子。固定种子是为了可复现,同一份代码、同一个种子,两次运行结果必须完全一致,这是验证代码没写崩的最低要求。-O2:不要用-O3做基准测试,激进优化可能把浮点逻辑重排,导致同一份代码在不同机器上结果出现微小差异。
编译阶段最常见的报错是random头文件找不到,说明编译器版本太旧,只支持 C++11 之前的语法。另外如果源码里用了<filesystem>,需要 C++17 并确保链接器支持,某些老版本工具链会要求附加-lstdc++fs。
2.3 随机数质量决定了整个模拟的可信度
这部分是很多人踩坑的地方。我见过不止一份抽卡模拟代码直接用rand() % 100判概率,然后拿结果当结论。这种写法在 Windows 上 RAND_MAX 只有 32767,且低位随机性很差,连续调用会出现肉眼可见的周期性。用它跑出来的金色分布是假的。
我的写法是固定用std::mt19937_64配合std::uniform_real_distribution<double>:
#include <random> #include <iostream> class Rng { public: explicit Rng(uint64_t seed) : engine_(seed) {} // 返回 [0.0, 1.0) 之间的双精度随机数 double next() { return dist_(engine_); } private: std::mt19937_64 engine_; std::uniform_real_distribution<double> dist_{0.0, 1.0}; }; // 单抽概率判定:roll < rate 视为命中 bool hit(double rate, Rng& rng) { return rng.next() < rate; }三个细节值得说透。其一,不要每次抽卡都重新构造引擎,构造时播种会让序列变得可预测,正确的做法是全局或成员持有一个引擎实例。其二,uniform_real_distribution在标准里不保证跨平台逐位一致,如果你的实验需要跨机器对比结果,就自己用整数做映射,比如把engine_()的 64 位输出除以UINT64_MAX。其三,概率判定的方向要统一,全篇都用roll < rate,一旦某处写成>,那一支的概率就整个反了,而这种错在小样本下几乎看不出来。
3. 把保底与软保底写进参数表:改一处,跑一万次
规则读懂了,接下来才是这个模拟器真正好玩的地方——把参数从代码里挖出来,做成可配置的输入,然后跑不同的假设,看最终期望抽数和分布形状怎么变。硬编码的模拟器只能演示,参数化的模拟器才能做实验。
3.1 参数化设计:配置结构体比魔法数字好维护
我一般会定义一个GachaConfig,把所有概率和阈值集中在一处。好处不只是好改,更重要的是改完能一眼看出哪些参数互相关联:
struct GachaConfig { int hardPity = 90; // 硬保底抽数 int softStart = 74; // 软保底起始抽数 double baseRate = 0.006; // 基础出金概率 double softStep = 0.06; // 软保底每抽递增概率 double rateUpChance = 0.5; // 小保底命中当期概率 double maxSoftRate = 1.0; // 概率上限,防止超过 1 }; // 根据当前 pity 计算本抽出金概率 double currentRate(const GachaConfig& cfg, int pity) { if (pity + 1 >= cfg.hardPity) return 1.0; if (pity + 1 < cfg.softStart) return cfg.baseRate; double r = cfg.baseRate + cfg.softStep * (pity + 1 - cfg.softStart + 1); return std::min(r, cfg.maxSoftRate); }注意这里pity + 1的用法:pity表示「已经抽了多少次没出金」,所以本次是第pity + 1抽。这个下标差一错,会让整条软保底曲线整体平移一抽,最终期望抽数偏移约 1 抽,但分布图上看不出来。我的做法是在打印日志时同时输出pity和本次是第几抽,跑小样本时肉眼核对前 90 条日志。
softStep是整套参数里最玄学的一个。按 0.06 的等步长叠加,第 74 抽是 6.6%,到第 89 抽接近 100%,综合下来金色期望大概落在 62 到 63 抽之间,和流传的公开统计对得上。你要是把它改成 0.07,期望会掉到 60 抽以下,明显偏离。所以这个参数不是随便填的,它本质上就是用递推曲线去拟合公开统计量。
3.2 跑批量实验:命令行参数与结果解释
参数化之后,批跑就有了意义。我常用的三组对照实验:
# 场景 A:基准模型 ./gacha --pulls 1000000 --seed 42 --soft-step 0.06 --rate-up 0.5 # 场景 B:软保底更激进 ./gacha --pulls 1000000 --seed 42 --soft-step 0.07 --rate-up 0.5 # 场景 C:验证大保底对期望的影响 ./gacha --pulls 1000000 --seed 42 --soft-step 0.06 --rate-up 0.5 --guarantee-on每组跑完,重点看四个输出:金色总数、平均出金间隔、歪的比率、第 90 抽硬保底的触发次数。前两个验证综合概率,第三个验证 50/50 是否实现正确,第四个如果接近零,说明软保底曲线已经把概率堆得太满,硬保底几乎轮不到出手——这也是一种合理性检验。
固定种子下重复跑,结果必须一模一样。如果两次结果不同,问题一般出在两个地方:多线程共用了一个随机引擎却没有加锁,或者某处用了rand()之类全局状态。这属于血泪经验,我在别处被这类隐蔽的并发问题坑过一次,查了一整天才定位到。
3.3 统计口径:为什么只看「出金率」会得出错误结论
很多人跑完模拟就看一个数:金色占比。这个数几乎恒定在 1.6% 附近,看不出任何东西,因为它是被硬保底强行拉平的。真正有区分度的是「出金抽数分布」——把每一次出金距离上次出金的间隔记录下来,画成直方图,你会看到一根在 74 到 89 之间的陡峭上升沿,以及 90 处的一个小尖峰。
我的统计结构长这样:
struct Stats { std::vector<int> goldIntervals; // 每次出金消耗的抽数 int totalPulls = 0; int goldCount = 0; int lostFifty = 0; // 小保底歪的次数 double avgInterval() const { if (goldIntervals.empty()) return 0.0; long long sum = 0; for (int v : goldIntervals) sum += v; return static_cast<double>(sum) / goldIntervals.size(); } };注意avgInterval用long long累加:一百万抽、金色一万六千次、间隔总和接近一百万,int在极端参数下存在溢出风险,虽然概率低,但这种溢出属于静默错误,比崩溃更难查。另外lostFifty这个计数器必须只在小保底出金时增加,大保底出金不能计入,否则歪率会被系统性低估。
有了这个统计结构,你就能做真正的对照:把softStep从 0.05 到 0.09 每隔 0.01 跑一遍,记录平均间隔,画出来是一条单调下降的曲线,斜率大约每 0.01 对应 3 到 4 抽。这条曲线就是你判断“某个流传的保底规则是否可信”的依据。
4. 避坑:跑抽卡模拟时最容易翻车的五件事
模拟器本身不复杂,但结论出错往往不是代码写错,而是用法错了。下面五条是我实际跑这类实验时反复遇到的,按「现象 → 原因 → 解决」记下来。
4.1 结果每次都不一样,无法复现
现象:同一份代码、同样的命令,跑两遍平均出金间隔差了一两抽。原因:种子没有固定,或者代码里某处偷偷用了std::random_device作为默认种子,把命令行传的 seed 覆盖掉了。解决:在入口处把 seed 打印出来,并在所有随机数构造点检查是否用了传入值;另外确认没有代码路径在运行中途重新播种。
4.2 小样本结论被当成规律
现象:跑 1000 抽,出金间隔平均 40 抽,于是断定模型偏乐观。原因:1000 抽只有十几个金,样本量根本不够,单次实验的标准误极大。解决:至少上到十万抽量级,并且用多个不同种子各跑一遍,看结果的波动范围,而不是盯单次结果。我习惯用 5 个种子、每个 20 万抽,取均值再比较。
4.3 概率累加超过 1,软保底末端行为异常
现象:第 88、89 抽附近出金概率超过 100%,随机判定逻辑变得无意义。原因:等步长叠加没有做上限截断,baseRate + softStep * n一路加过了 1。解决:如前面代码所示,用std::min截断到 1.0,并且确保硬保底判断放在最前面优先返回。
4.4 大保底状态在跨池或跨轮次时没重置
现象:连续模拟多轮,歪率明显低于 50%。原因:guarantee标志在某条路径上没有在出金后清零,导致下一轮直接继承了大保底,等于凭空多送了一次必中。解决:把「出金后统一收尾」抽成一个函数,pity清零、guarantee按本次是否歪来置位,所有出金路径都必须走这个函数,不要在各分支里各写一遍。
4.5 用整数取模代替浮点判定
现象:改小概率之后,出金率纹丝不动,或者干脆变成 0。原因:rand() % 100 < rate这种写法在rate小于 1 时会失效,整数截断直接把 0.6% 变成 0。解决:统一用浮点判定rng.next() < rate,并且不要用整数百分比去近似千分位以下的小概率,0.006 和 0.01 差得很远。
提示:每次改完参数,先跑 1000 抽把前 90 条日志打出来看,确认软保底从第 74 抽开始爬升、第 90 抽强制出金,再放大样本。这一步花两分钟,能省掉后面半天的排查。
5. 从「跑通」到「跑准」:分布校验与长期复现习惯
模拟器跑通只是及格线,跑准才算数。我判断一个抽卡模拟是否可信,不看它输出的平均抽数,而是看它能不能复现三件事:出金间隔的分布形状、74 抽前后概率的拐点、90 抽处的硬保底尖峰。这三样对上了,模型大概率是稳定的;对不上,改参数前先怀疑代码。
具体校验方法我一般用两步。第一步做分桶统计,把出金间隔按区间计数,输出成表格,和人尽皆知的分布规律比对:
| 间隔区间 | 理论特征 | 校验关注点 |
|---|---|---|
| 1–73 抽 | 近似几何分布,尾部极薄 | 出现频率应很低,若明显偏高说明基础概率设大了 |
| 74–89 抽 | 概率逐抽陡升 | 应形成集中区,是整体分布的主峰 |
| 90 抽 | 硬保底强制触发 | 应有独立小尖峰,完全消失说明软保底过早堆满 |
| 出金后 | pity立即归零 | 间隔统计不应出现跨轮次粘连 |
第二步做参数敏感性扫描,把softStep从 0.04 扫到 0.10,每档跑 20 万抽、5 个种子,记录平均间隔和 90 抽触发占比。你会得到一条清晰的反向关系:softStep越大,平均间隔越短,硬保底占比越接近于零。如果某档参数下平均间隔反而变长,那一定是代码里出现了分支遗漏,比如软保底区间判断用了闭区间导致第 74 抽被跳过。
# 参数扫描示例:循环调用并汇总,结果追加到 csv for s in 0.04 0.05 0.06 0.07 0.08 0.09 0.10; do for seed in 1 2 3 4 5; do ./gacha --pulls 200000 --seed $seed --soft-step $s --csv >> sweep.csv done done这个脚本没什么技巧,价值在于把「一次性实验」变成「可重复的实验记录」。csv 里带上softStep和seed两列,之后不管隔多久回来,都能重新画出同一张图。
最后说个我自己的习惯。这个模拟器我改过三轮参数,第一轮改完看着平均抽数对上了就收工,结果第二轮发现硬保底触发次数是零,才知道软保底曲线早就把概率堆到了 100%。从那以后我每次改参数,都强制走一遍「1000 抽打日志 → 看前 90 条 → 再上大样本」这条流程,绝不跳过小样本核对。抽卡模拟这种活儿,数值看着都像对的,错的地方往往藏在你看不见的那一抽里。希望帮到你。
本文还有配套的精品资源,点击获取