
简介《原神抽卡模拟器》是一份基于C实现的祈愿抽卡模拟项目面向C初学者与原神玩家通过命令行或简单界面还原游戏内角色、武器抽取流程核心涵盖概率分布、保底机制与统计记录。压缩包共38个文件大小约4.04MB包含C源代码.cpp/.hpp/.h、Visual Studio工程配置.vcxproj/.sln/.filters、编译中间文件.obj/.pdb/.tlog以及可直接运行的exe程序另附MD文档和TXT数据文件适合对照学习和二次修改。该资源已有1576人学习浏览能够帮助理解C的随机数生成、类封装、文件读写和基础游戏逻辑设计。解压后可查看祈愿池定义、概率判断、保底计数等核心代码并通过编译产物快速运行体验MD说明与日志文件也有助于梳理开发思路适合作为C课程设计或兴趣项目的参考资料。 “原神抽卡模拟器.zip”看到这个文件名懂的都懂。这个项目说白了就是自己动手复刻一个“米式抽卡”系统——五星概率、九十抽硬保底、五十概率歪、命座机制、抽卡记录、大数据统计全部自己写出来。比起在游戏里真金白银试水模拟器最大的价值就是能让你用零成本把抽卡机制的底层逻辑彻底摸透顺便还能解决一个实际问题到底还差多少抽能出金垫了多少发手头的原石该怎么规划才不亏这篇文章从项目拆解、概率模型、技术实现到踩坑排查完整记录下我这个版本的实现过程代码层面用的是前端单页方案解压即用没有后端依赖适合想练手、想搞明白抽卡机制、或者单纯想做一个“抽卡分析器”的开发者参考。1. 抽卡模拟器的核心逻辑与机制拆解1.1 米氏抽卡的本质概率模型与保底规则做模拟器之前我先把原神的抽卡规则整个捋了一遍。五星角色的基础概率是0.6%但官方给出了一个“综合概率”1.6%两者之间的差异靠的就是保底机制来补齐。90发以内必出五星这就是硬保底。如果按最朴素的算法1 - (1 - 0.006)^90 ≈ 0.417跟1.6%的综合概率对不上。所以这里面一定还藏着一个隐性机制也就是玩家圈常说的“软保底”。软保底并没有官方文档直接说明它的来源主要是大量抽卡数据的逆向统计社区通过曲率计算发现在73或74抽之后实际出金概率会开始阶梯式上涨到89抽时几乎接近100%。这个机制解决了一个纯概率模型的致命问题——如果92发出了一个五星玩家的体验会极其糟糕。保底规则把“最坏情况”锁死在一个可控范围内而软保底则把实际平均出金抽数往官方公布的综合概率上靠。我的模拟器里把这个过程拆成两个独立的状态机一个是“抽数计数”一个是“UP标记”抽数计数决定你离保底还有多远UP标记决定出金那一刻你是小保底还是大保底两个状态互不干扰但共同决定最终结果。let pity 0; // 当前保底计数 let guaranteeUp false; // 是否为大小保底 let hardPity 90; // 五星硬保底 function drawFiveStar() { let p 0.006; if (pity 74) { // 模拟软保底pity越高概率线性抬升 p 0.006 (0.355 / 16) * (pity - 73); } if (Math.random() p || pity hardPity - 1) { return true; } return false; }这里有一个细节值得注意90发是“保底必出”的一次判定而不是说“第90发才开始计算”代码里把命中条件写成pity hardPity - 1意思是当你已经抽了89发还没出金时第90发的概率已经接近100%这符合实际体验。1.2 为什么用“抽卡模拟”而不是查概率表你如果只是想知道“多少抽能出金”那直接看概率表就够了完全不需要模拟器。但模拟器的价值不在于它给出了期望值而在于它展示了单次抽卡的真实分布。概率表告诉你的是一条平滑的期望曲线模拟器告诉你的则是某个具体玩家在第70抽出金、第85抽出金、甚至第90抽才出金的散点分布。不同的抽卡策略会对应完全不同的体验比如“攒到180抽再梭哈”和“有十连就随手抽空”长期下来五星数量可能接近但过程中你经历“连续吃满大保底”的概率完全不同这种体验差异只有跑模拟才能感受到。原神玩家圈里常说“垫池子”“捞四星”本质上就是在利用底层概率去优化自己的抽卡体验。模拟器把这些决策变成了可量化的测试在一个安全的、不需要充一分钱的环境里把所有可能发生的“歪”全部提前经历一遍。2. 技术选型与实现思路2.1 为什么用zip分发而不是在线版本我把项目命名为“原神抽卡模拟器.zip”本质上就是在强调它“绿色免安装、双击即用”的属性。一个压缩包解决了两个痛点一是用户不需要搭环境、不需要装依赖二是整个工具可以随时打包带走换台电脑直接解压就能跑。对于这种小体量的模拟器项目纯前端单页HTML JavaScript 是最合理的方案。不需要服务器、不需要数据库、不需要构建工具链一个index.html文件承载所有逻辑一个style.css控制界面样式用户本地打开文件后通过file://协议就能运行。如果做成在线版反而会引入跨域、部署、服务器维护等一系列问题对一个工具型项目来说是得不偿失的。不过我踩过一个坑直接用file://协议打开时浏览器对某些API有限制比如fetch在部分浏览器本地文件模式下会报CORS错误。所以我的实现中把所有数据都放在本地变量里用localStorage做存档完全不依赖网络请求。这也顺带回避了所有跨域问题。2.2 随机数引擎的选型Math.random()够用吗前端做概率模拟第一个想到的肯定是Math.random()。这个方法返回一个 [0, 1) 区间的伪随机浮点数对大多数场景足够用了。但它有一个问题不可复现。你没法给一次“抽卡过程”生成一个种子然后把这次抽卡过程完整回放给别人看。为了让模拟器具备“分享和复现”的能力我改用了可播种的伪随机数生成器PRNG。一个很经典的实现就是 mulberry32它只需要一个32位整数做种子就能生成周期长达 2^32 的伪随机序列速度很快、分布也还均匀。它最直观的价值是能让“抽卡记录”变成一串可以分享的数字。function mulberry32(seed) { return function() { seed | 0; seed seed 0x6D2B79F5 | 0; let t Math.imul(seed ^ seed 15, 1 | seed); t t Math.imul(t ^ t 7, 61 | t) ^ t; return ((t ^ t 14) 0) / 4294967296; }; }我这里把所有玩家交互都接入同一个随机数生成器每次抽卡前取下一个随机数这样你只要把种子号和抽卡次数记录下来任何时候重新跑一遍就能得到完全一致的结果。对“抽卡玄学”来说这种可复现能力意味着你可以直接验证某个流派是否真的能改变命运结果当然是不能但这个过程本身蛮有意思的。2.3 界面与数据层本地存储与抽卡记录抽卡模拟器是一个纯前端项目但你不能只做一个简单的按钮加数字显示。真正让项目有完整感的是“抽卡记录”这个模块。我把每次抽卡的结果以对象形式存进localStorage结构大概是{ id: uuid-..., seed: 20240517, pullIndex: 173, rarity: 5, isUp: true, name: 刻晴, timestamp: 1716100000000 }用localStorage的好处是简单坏处是它有容量上限大概在5MB到10MB之间如果你跑十几万次模拟记录数据会撑爆存储。我在实现里加了一个策略默认只保留最近1000条完整记录更早的记录只保留统计聚合结果不保留单条明细。这样既保留了近期可回看的数据又不会因为一个模拟器把浏览器撑爆。3. 核心功能落地保底、歪与命座实战3.1 常规池与UP池的完整状态机UP池和常驻池在底层逻辑上是完全不同的两套判定体系。UP池需要判断“是不是限定五星”常驻池则需要把五星结果从常驻角色池里随机指定一个。我总共维护了三个角色池子UP五星池、常驻五星池、四星池后两者需要按权重做随机选择。从实现上说UP池比常驻池多一个状态变量hasLostLast如果上次五星是非UP角色那这次出五星时必定进UP池。UP池的完整状态机如下let state { pity5: 0, pity4: 0, lostFifty: false, totalPulls: 0 }; function pullOnce() { state.pity5; state.pity4; state.totalPulls; if (state.pity5 90 || rand() getFiveStarRate(state.pity5)) { let isUp false; if (!state.lostFifty) { // 小保底50%概率出UP if (rand() 0.5) isUp true; else isUp false; } else { // 大保底必出UP isUp true; } state.lostFifty !isUp; state.pity5 0; state.pity4 0; return { rarity: 5, isUp: isUp }; } // 四星判定类似十抽保底 ... }这里的lostFifty是整个状态机最核心的一个变量出五星时如果它之前是false说明你处在小保底状态此时50%出UP、50%歪掉并把它置为true如果之前是true说明你上一次已经歪掉了这一次必定出UP同时把它重置回false。这个规则和游戏里完全一致。3.2 十连抽与单抽的底层一致性很多玩家认为十连和单抽概率不同但从代码角度来说十连只是单抽的批量循环并不会因为“批量”改变任何概率参数。我在实现里强制保持这一点因为任何不一致都会破坏数据的整体期望值。十连抽比较好玩的地方在于它的动画交互你可以做成每张卡依次翻转的效果也可以做成一次开十张的快速生成。我这里做了个懒加载方案十连开抽时先按顺序生成10个结果然后每隔200毫秒渲染一张卡牌这样既保证了抽卡的紧张感又不会因为一次性渲染10张卡造成界面卡顿。function simulateTenPull() { const results []; for (let i 0; i 10; i) { results.push(pullOnce()); } return results; }如果纯粹从概率上看单抽和十连确实完全等价但我在模拟器里发现了一个很有意思的心理差异单抽的时候玩家很容易在出金前的“铺垫期”产生烦躁十连却能在第8抽、第9抽连续出紫的瞬间营造出一种“差一点就出金”的错觉这种错觉会让抽卡动机维持得更久。游戏厂商当然深知这一点这也是十连按钮存在的意义之一。3.3 四星保底与命座系统的设计五星之外的四星保底也值得做做到位。原神的规则是10抽以内至少出一个四星或以上物品。这里的“或以上”非常关键如果你的第9抽出了五星四星保底计数会跟着重置而不是给你额外再补一个四星。我早期实现时把四星和五星的计数分开处理结果出现了“第9抽出五、第10抽保底四”的bug导致四星整体产率高于理论值。命座系统的设计也是模拟体验的一部分。在游戏里每抽到一个已拥有的角色会转化为对应的命座角色本身不再增加。模拟器里我维护了一个ownedCharacters集合每次抽出四星时先判断是否已经拥有如果已拥有则增加该角色的constellation计数。这个功能的实际用途是模拟“满命”成本——目前满命一个五星需要7个本体最非的情况下会吃两次大保底总计高达 180 × 2 90 450抽以上。模拟器可以很直观地计算出满命一个限定角色的大概预算对想规划抽卡的人来说是个非常实用的参考。3.4 抽卡记录导出与URL分享我希望让用户能把一次“欧气”或者“非气”完全发给朋友于是设计了一个分享URL的模块。实现方式是把抽卡序列编码成Base64格式拼到URL参数里对方打开URL后会自动解析并导出抽卡记录。const data JSON.stringify(pullHistory); const encoded btoa(data); const shareUrl location.origin location.pathname ?record encoded;不过传入URL的参数有长度限制一般建议不超过2000字符。如果整段抽卡记录太长就直接压缩成统计摘要来分享——比如“500抽出金5歪了2次综合概率1.0%”这种形式同样是够好玩且实现成本很低的特性。4. 从娱乐工具到数据分析器抽卡策略的量化验证4.1 策略一见好就收还是梭哈到底模拟器最有意思的价值是能验证各种抽卡玄学策略的实际收益差异。我在项目里设置了一个“策略模拟”模块可以配置两种模式模式A每UP池只抽到出金就停模式B不管出不出金攒够一定数量的抽数就全部投入以200抽预算为例模式A会分散到多个UP池中好处是每个池子都有可能小保底出货模式B则是集中到某一个池子里硬吃保底的概率更高但如果中了就是大保底全拿。通过几千次蒙特卡洛模拟你会发现两种策略的期望五星数其实相差无几但方差差异非常大——模式A的抽数分布更分散连续歪掉的体验更少模式B虽然可能有某期非常欧但更容易出现极端非的时期。这些结果完全可以量化成表格我在模拟器里加入了一个“期望结果”面板会显示这200抽下来0金、1金、2金、3金、4金及以上的概率分布玩家可以看到自己处于哪个位置。4.2 用大样本反推验证官方概率抽卡模拟器一定意义上也是概率验证工具。我用它跑了100万次模拟把统计结果和官方公布的数据对照过一轮指标官方期望100万次模拟结果五星综合概率约1.6%1.605%平均出金抽数约62.362.3770抽出金占比约40%39.7%90抽出金占比100%100%这里的关键在于软保底曲线的参数设置。如果你只用0.6%基础概率和90抽硬保底模拟出来的综合概率只有约0.417/90 ≈ 0.46%远低于1.6%的官方值。只有引入74抽后概率逐步提升的模型整体期望才能被拉到 62.5 抽左右。所以模拟器不仅是游戏的复刻更是对游戏概率模型的逆向工程。这一块做下来你能学到的最有价值的东西是“期望值”与“真实体验”之间的落差大多数玩家的“心理出金点”会落在70-80抽附近而官方用软保底曲线让这个区间的出金占比显著提高从而让“快要保底了”的期待感真实存在。4.3 对“抽卡策略”的日常理解修正还有一个有趣的发现随机数生成器看起来很好地模拟了赌徒心理但实际上玩家更常低估大保底的成本。模拟器的数据显示如果一个玩家连续抽了6个五星并且全歪了概率大约是 (0.5)^6 1.5625%100人里就能有1-2个人遇到这种情况并不算极端。很多人说“不可能连续歪这么多次”但概率论就不这么认为。模拟器最大的价值就是让你在“安全环境”下体验这种概率波动从而对真实游戏中的沉没成本有一些更清醒的认识。5. 常见问题与排查技巧实录5.1 zip解压环节的坑项目既然叫“原神抽卡模拟器.zip”解压就是一个绕不开的环节。之前我发布过一个版本压缩包里的主文件名是中文“抽卡模拟器.exe”结果有用户反馈解压后打不开。排查后发现问题出在较多老旧的压缩工具对中文文件名的编码兼容不好默认用GBK编码而现代工具用UTF-8两个环境切换后文件名会乱码最终导致路径解析失败。后来我把所有文件名都改成英文字符index.html、app.js、style.css彻底解决了跨平台解压乱码的问题。另外在打包时用ZIP标准格式不要用7z自己的压缩算法否则用户需要额外装软件才能解开这与“绿色免安装解压即用”的初衷就违背了。一些用户反馈解压时出现“could not find EOCD”类似报错这通常是压缩包在传输过程中被截断或者下载不完全导致的。我的建议是重新下载一份然后在本地校验一下文件大小是否与发布页标注一致这种问题九成是下载环节的问题不是压缩包本身损坏。提示如果解压工具报错先把压缩包放到本地磁盘再解压不要在在线网盘目录里直接双击解压很多网盘客户端会拦截格式头导致EOCD识别失败。5.2 随机性“太假”怎么办有用户反馈模拟器“总是连续出金”或者“很长一段时间都不出金”怀疑随机数分布有问题。这其实是一次小样本偏差一个70抽出金的概率本来就不低连续几次70抽也完全合理但人的直觉会觉得这是“设置问题”。如果你想让结果看起来更“真实”可以引入“保底计数平滑”方案把连续两次出金的概率峰值做平滑处理强制减少“双黄”出现的概率让结果分布更接近人的直觉。但这实际上背离了真实概率模型游戏中双黄本来就存在。我最终选择保留真实模型只是在界面上增加一个统计曲线图让用户直观看到当前抽数和历史出金位置的分布关系减少“随机感”带来的错觉。5.3 抽卡数据超过localStorage容量前面提过localStorage有5MB左右的容量限制如果你存了太多历史记录写入时会抛异常。这个问题很隐蔽浏览器不弹出错误只是静默失败。我用try-catch捕获写入异常后做了一个自动降级策略如果发现写入失败就把最老的记录移除再重试直到写入成功为止。function saveRecord(record) { let history JSON.parse(localStorage.getItem(history) || []); history.push(record); while (history.length 1000) { history.shift(); } try { localStorage.setItem(history, JSON.stringify(history)); } catch (e) { history history.slice(-500); localStorage.setItem(history, JSON.stringify(history)); } }这个降级策略既保证了高频使用时的体验也避免用户因为数据过大造成页面崩溃。毕竟模拟器的核心诉求是“随手抽两发看看感觉”不是长期记账本保留1000条也就足够了。5.4 四星与五星重复触发Bug开发过程中我遇到最隐蔽的一个逻辑Bug是四星和五星共用了一个“十抽必出四星”的计数器。用户“垫池子”时抽了8发没出紫第9发直接出了金按规则应该同时把四星保底计数重置为0但我的初始化代码把四星保底计数重置成了1导致下一次十连必出紫的判定出错。排查这类问题最好的方式就是加一个断点模拟器按单抽模式逐步执行每抽一次打印出两个计数器的值非常直观地就能看出状态流转是否异常。这类问题也提醒了我状态变量之间一定要理清依赖关系五星、四星的保底计数表面上独立实际上因为“出金时必定重置四星计数”这个规则产生了耦合任何先后顺序写反都会导致概率偏移。写在最后的一个小经验这个模拟器项目做到后期给我最大的感触是单一“抽卡玩具”的价值其实有限真正有价值的是概率建模的思维方式。你从“感觉概率有问题”到用大样本数据去验证再到调整软保底参数逼近官方值这个过程本身就是一个完整的 假设 - 建模 - 验证 - 修正 循环。很多人觉得概率论枯燥但当你亲自实现一个抽卡系统、看到累积概率曲线向着期望值收敛的时候这种“意料之外、情理之中”的感觉真的很奇妙。如果接下来你还想继续扩展这个项目我建议可以试试把模拟器做成一个“抽卡预算规划器”输入玩家的月卡、纪行资源获取速度自动计算某个版本的攒抽曲线再结合角色复刻预测数据推荐抽取优先级。这套东西做出来实用价值会比模拟器本身高出一个量级。本文还有配套的精品资源点击获取