☰
原神抽卡模拟器开发指南:概率模型、代码实现与验证
2026/10/25 13:29:49 网站建设 项目流程

简介:这是一份面向C++初学者与原神玩家的抽卡模拟器源码工程,用C++复刻游戏内祈愿逻辑,帮助学习者在无需消耗原石的前提下理解抽卡概率与保底机制。资源以Visual Studio解决方案形式组织,包含main.cpp、wish_function.cpp、wish.h、cmd_login.hpp、Md5.hpp等核心源码与头文件,并附带数据文本、登录日志及编译产物,可直接打开工程编译运行。压缩包共38个文件,以txt与tlog日志、cpp与hpp源码、pdb与obj编译中间文件为主,另有sln、vcxproj工程配置及exe可执行文件,整体约4.04MB。目前已有1592人学习下载。读者可从中掌握用random库生成随机数、设计祈愿池与保底计数器、统计出货率等实现思路,并参考登录与MD5模块了解工程结构,适合作为C++面向对象与概率统计的练手项目。

1. 原神抽卡模拟器.zip:一个压缩包背后到底藏着什么

你下载了一个叫「原神抽卡模拟器.zip」的压缩包,解压后可能是一堆 HTML、JS 文件,也可能是一个 Python 脚本加几张角色立绘。不管哪种形态,它本质上都在做同一件事:用代码复现《原神》里「祈愿」系统的概率逻辑和交互反馈。很多人第一次搜「原神抽卡模拟器」,动机很朴素——想看看自己到底是欧皇还是非酋,又不想真金白银往里砸。这个需求非常合理,因为原神抽卡的概率模型本身就是一个值得拆解的工程问题:基础概率 0.6%,74 抽后概率线性递增,90 抽硬保底,大保底机制、四星保底、十连保底……这些规则用代码写出来并不复杂,但要把「模拟器」做得可信、可玩、可验证,里面有不少细节值得聊。这篇文章面向两类人:一是想自己动手写一个抽卡模拟器的开发者,二是拿到别人写的模拟器但不确定它靠不靠谱的玩家。我会从概率模型怎么建、代码怎么写、参数怎么调、哪里容易翻车,一路讲到怎么验证你的模拟器是不是真的「像那么回事」。

2. 抽卡概率模型:从官方规则到可执行代码

2.1 原神祈愿的概率规则到底怎么描述

在动手写代码之前,必须先把规则用自然语言精确描述出来,否则代码一定写歪。原神角色活动祈愿(限定池)的核心规则可以归纳为以下几点:

  • 五星角色基础概率为 0.6%,从第 74 抽开始,每抽概率增加 6%,直到第 90 抽达到 100%。
  • 五星物品有保底机制:如果上一次五星不是当期 UP 角色,则下一次五星必定是 UP 角色,这就是俗称的「大保底」。
  • 四星物品基础概率为 5.1%,每 10 抽至少出一个四星或以上物品。
  • 四星同样有 UP 机制:三个四星 UP 物品共享 50% 的概率,如果上一次四星不是 UP,则下一次四星必定是 UP 之一。

这些规则看起来清晰,但落到代码里有一个关键分歧:概率递增是从第 74 抽开始算,还是从第 75 抽开始算?不同版本的模拟器在这点上处理不一样。常见做法是定义一个soft_pity_start = 74,然后第 n 抽的五星概率为0.006 + max(0, n - 74) * 0.06,当 n 达到 90 时直接返回 1.0。这个公式在 n=74 时概率仍是 0.6%,n=75 时变成 6.6%,以此类推,到 n=90 时正好是 0.006 + 16 * 0.06 = 0.966,所以第 90 抽必须强制为 1.0。这个细节如果不处理好,模拟出来的出货分布会和真实体感有明显偏差。

2.2 用 Python 写一个最小可用的抽卡核心

下面是一个抽卡核心逻辑的最小实现,不依赖任何第三方库,直接跑就能用:

import random class GachaSimulator: def __init__(self): self.pity_5star = 0 # 距离上次五星的抽数 self.pity_4star = 0 # 距离上次四星的抽数 self.guaranteed_5star = False # 是否处于大保底 self.guaranteed_4star = False # 是否处于四星大保底 def _five_star_rate(self): """计算当前抽的五星概率""" n = self.pity_5star + 1 if n >= 90: return 1.0 if n <= 74: return 0.006 return min(0.006 + (n - 74) * 0.06, 1.0) def _four_star_rate(self): """计算当前抽的四星概率""" n = self.pity_4star + 1 if n >= 10: return 1.0 return 0.051 def pull_one(self): """单抽,返回 ('5star'/'4star'/'3star', 是否为UP)""" # 先判定五星 if random.random() < self._five_star_rate(): self.pity_5star = 0 self.pity_4star += 1 is_up = self.guaranteed_5star or random.random() < 0.5 self.guaranteed_5star = not is_up return ('5star', is_up) # 再判定四星 if random.random() < self._four_star_rate(): self.pity_4star = 0 self.pity_5star += 1 is_up = self.guaranteed_4star or random.random() < 0.5 self.guaranteed_4star = not is_up return ('4star', is_up) # 三星 self.pity_5star += 1 self.pity_4star += 1 return ('3star', False) def pull_ten(self): """十连,返回结果列表""" return [self.pull_one() for _ in range(10)]

这段代码的逻辑说明:pity_5star记录的是「距离上一次五星已经过了多少抽」,所以在计算当前抽概率时要用pity_5star + 1作为当前是第几抽。五星判定优先于四星,这是符合官方规则的——如果一抽同时满足五星和四星的条件,它算五星。guaranteed_5star标记大保底状态,当上一次五星不是 UP 时置为 True,下次五星必定 UP。四星逻辑同理,但四星没有「软保底递增」,只有 10 抽硬保底。

参数方面,0.006和0.06这两个数字是核心,改任何一个都会显著影响出货分布。0.051是四星基础概率,10是四星保底抽数。如果你想让模拟器更接近真实体感,不要动这些值,而是去调整随机数生成器的种子或者跑更多次模拟来观察分布。

2.3 十连保底和单抽的差异在哪

很多人以为十连就是「抽十次单抽」,从概率上讲确实如此,但有一个细节:十连保底确保的是「十抽内至少有一个四星或以上」,而不是「第十抽必定四星」。上面的pull_ten实现是逐抽调用pull_one,四星保底逻辑在_four_star_rate里已经处理了——当pity_4star + 1 >= 10时概率为 1.0。这意味着如果你前九抽都没出四星,第十抽必定是四星或五星。这个逻辑和单抽完全一致,所以十连和单抽在概率上没有区别,只是展示形式不同。

但有一个容易翻车的点:如果你在十连过程中出了五星,pity_4star会加 1 而不是清零,这会导致四星保底计数继续累积。这是正确的,因为五星不重置四星保底。但有些模拟器实现里会把五星也当作「重置四星保底」来处理,这就错了,会导致四星出货率偏低。

3. 把模拟器做成能看的界面:HTML 单文件方案

3.1 为什么推荐单文件 HTML 而不是框架

如果你只是想做一个自己能玩、能分享给朋友的抽卡模拟器,单文件 HTML 是最务实的选择。不需要 Node.js、不需要构建工具、不需要服务器,双击就能跑,发给别人也能直接打开。用 React 或 Vue 当然可以,但你会花大量时间在环境配置和打包上,而抽卡模拟器的核心价值在概率逻辑和交互反馈,不在技术栈。常见做法是:一个 HTML 文件,内联 CSS 和 JavaScript,用 Canvas 或 DOM 做抽卡动画,用 localStorage 存保底计数。

3.2 用 Canvas 做十连翻牌动画的关键参数

十连抽的视觉反馈是模拟器「像不像」的关键。原神的十连是先显示一张一张卡片背面,然后依次翻开。用 Canvas 实现时,核心参数包括:

参数建议值说明
卡片宽度120px十连时一行五张,两行
卡片高度200px宽高比接近原神卡面
翻牌间隔80ms太快没感觉,太慢烦躁
五星特效时长1200ms金光闪烁加粒子
四星特效时长600ms紫光一闪
三星过渡200ms快速翻过

这些值不是绝对的,但如果你想让模拟器有「那味」,翻牌间隔不要低于 50ms,五星特效不要短于 800ms。很多模拟器翻车就翻在动画太快,十连一秒翻完,完全没有抽卡的感觉。

3.3 用 localStorage 存保底状态的最小实现

抽卡模拟器如果不存保底状态,每次刷新页面都重置,那就失去了「模拟」的意义。用 localStorage 存状态是最简单的方案:

// 保存状态 function saveState(state) { localStorage.setItem('gacha_state', JSON.stringify({ pity5: state.pity5, pity4: state.pity4, guaranteed5: state.guaranteed5, guaranteed4: state.guaranteed4, totalPulls: state.totalPulls })); } // 读取状态 function loadState() { const raw = localStorage.getItem('gacha_state'); if (!raw) return { pity5: 0, pity4: 0, guaranteed5: false, guaranteed4: false, totalPulls: 0 }; try { return JSON.parse(raw); } catch (e) { // 数据损坏时重置,避免白屏 return { pity5: 0, pity4: 0, guaranteed5: false, guaranteed4: false, totalPulls: 0 }; } }

逻辑说明:saveState在每次抽卡后调用,把当前保底计数和总抽数写入 localStorage。loadState在页面加载时调用,如果解析失败就返回默认状态。这里有一个坑:如果你在loadState里不做 try-catch,一旦 localStorage 里的数据被手动改坏或者版本不兼容,整个页面会直接报错白屏。加上异常处理是血泪经验。

参数方面,totalPulls用来统计总抽数,可以用来算平均出货率。guaranteed5和guaranteed4是布尔值,存的时候用 JSON 序列化没问题。注意不要存函数或循环引用,否则JSON.stringify会报错。

4. 避坑与排查:抽卡模拟器最容易翻车的 5 个地方

4.1 概率递增公式写错导致出货分布异常

现象:模拟器跑一万次,平均五星出货抽数在 55 左右,而真实体感应该在 62 到 65 之间。

原因:概率递增的起始抽数写成了 70 或者递增步长写成了 0.05。原神的软保底是从第 74 抽开始,每抽增加 6%,这两个数字任何一个写错都会让出货偏早。

解决:把soft_pity_start固定为 74,increment固定为 0.06,然后在第 90 抽强制概率为 1.0。跑 10000 次模拟,统计五星出货抽数的分布,如果中位数在 75 到 80 之间,说明公式对了。

4.2 大保底状态在十连中丢失

现象:十连抽到五星后,下一次五星仍然可能歪,大保底没有生效。

原因:十连实现里每次pull_one都重新读取状态,但状态没有在十连结束后正确写回。或者guaranteed_5star在十连过程中被覆盖了。

解决:确保pull_one修改的是同一个状态对象,十连结束后统一保存。不要在十连里创建新的模拟器实例。

4.3 四星保底被五星重置

现象:四星出货率明显偏低,十连里经常只有一个四星甚至没有。

原因:代码里在判定五星后把pity_4star清零了。正确做法是五星不重置四星保底,只有四星才重置。

解决:检查pull_one里五星分支,确保pity_4star是加 1 而不是清零。

4.4 localStorage 数据损坏导致页面白屏

现象:刷新页面后模拟器直接白屏,控制台报 JSON 解析错误。

原因:localStorage 里的数据被手动修改过,或者旧版本的数据结构和新版本不兼容。

解决:在loadState里加 try-catch,解析失败时返回默认状态并清除损坏数据。同时给存储的 key 加版本号,比如gacha_state_v2,版本升级时旧数据自动失效。

4.5 动画卡顿导致抽卡体验断裂

现象:十连翻牌时动画掉帧,五星特效卡住不动。

原因:用 DOM 操作逐张卡片添加类名,每次添加都触发重排重绘。或者用setTimeout控制动画时序,但时间间隔太短导致回调堆积。

解决:用 Canvas 绘制动画,或者用 CSStransform和opacity做硬件加速。翻牌间隔不要低于 50ms,五星特效可以用requestAnimationFrame驱动粒子效果,避免用setInterval。

5. 验证模拟器可信度的三个硬指标

5.1 跑一万次模拟,看五星出货抽数的分布

一个可信的抽卡模拟器,跑 10000 次单抽(或 1000 次十连),五星出货抽数的分布应该满足:中位数在 75 到 80 之间,平均出货抽数在 62 到 65 之间,90 抽保底触发的比例在 5% 到 8% 之间。如果你模拟出来的平均出货抽数低于 55,说明概率递增太早;如果高于 70,说明递增太晚或者保底没生效。

import statistics def run_simulation(n=10000): sim = GachaSimulator() pulls = [] current = 0 for _ in range(n): current += 1 result, _ = sim.pull_one() if result == '5star': pulls.append(current) current = 0 return pulls pulls = run_simulation(10000) print(f"平均出货抽数: {statistics.mean(pulls):.2f}") print(f"中位数: {statistics.median(pulls):.2f}") print(f"90抽保底触发次数: {pulls.count(90)}")

这段代码跑完后,如果平均出货抽数在 62 到 65 之间,中位数在 75 到 80 之间,说明你的概率模型基本可信。如果偏差超过 5%,回去检查_five_star_rate的公式。

5.2 用卡方检验验证四星出货率

四星基础概率 5.1%,加上 10 抽保底,实际四星出货率应该在 13% 左右。你可以跑 10000 次单抽,统计四星出货次数,然后用卡方检验判断是否显著偏离预期。如果四星出货率低于 10%,大概率是四星保底逻辑写错了。

5.3 大保底触发率的验证方法

大保底的理论触发率是 50%——每次五星有 50% 概率是 UP,如果不是 UP,下次必定 UP。所以长期来看,UP 五星占总五星的比例应该接近 75%(第一次 50% UP,剩下 50% 里第二次必定 UP,所以总 UP 率 = 50% + 50% × 100% = 75%)。如果你模拟出来的 UP 比例在 70% 到 80% 之间,说明大保底逻辑正确。低于 70% 说明大保底没生效,高于 80% 说明 UP 概率写成了 100%。

我自己的习惯是每次改完概率逻辑,先跑 10000 次模拟看三个指标,指标不对就不往下做界面。这个习惯帮我省了很多后悔药——界面做得再漂亮,概率错了就是自欺欺人。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询